diff --git a/.solhint.json b/.solhint.json index c0c4e08e7..b370ae52f 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,6 +1,7 @@ { "extends": "solhint:recommended", "plugins": ["prettier"], + "excludedFiles": ["**/src/Interfaces/**/*.sol"], "rules": { "avoid-call-value": "off", "avoid-low-level-calls": "off", diff --git a/config/global.json b/config/global.json index 2b04f3e97..3d842cf04 100644 --- a/config/global.json +++ b/config/global.json @@ -55,6 +55,11 @@ "Permit2Proxy", "TokenWrapper" ], + "ldaCoreFacets": [ + "DiamondCutFacet", + "DiamondLoupeFacet", + "OwnershipFacet" + ], "blacklistedFunctionSelectors": ["0x23b872dd"], "whitelistPeripheryFunctions": { "FeeCollector": [ diff --git a/conventions.md b/conventions.md index 30b93b642..08c675748 100644 --- a/conventions.md +++ b/conventions.md @@ -154,6 +154,7 @@ We use Foundry as our primary development and testing framework. Foundry provide - **Error Handling:** - **Generic errors** must be defined in `src/Errors/GenericErrors.sol` + - LDA-specific errors should be defined in `src/Periphery/LDA/LiFiDEXAggregatorErrors.sol` - Use for common validation errors that apply across multiple contracts - When adding new generic errors, increment the version in `@custom:version` comment - Examples: `InvalidAmount()`, `InvalidCallData()`, `UnAuthorized()` @@ -263,6 +264,15 @@ We use Foundry as our primary development and testing framework. Foundry provide /// @return Description of return value /// @dev Additional details about implementation (optional) ``` + - Incorrect format (do not use): + ```solidity + /** @notice Brief description of function purpose + * @param parameterName Description of parameter + * @return Description of return value + * @dev Additional details about implementation (optional) + */ + ``` + - Always use `///` single-line format instead of `/** */` block format for better readability and gas efficiency 4. **Complex Logic Documentation** - Add inline comments for complex algorithms @@ -369,6 +379,191 @@ We use Foundry as our primary development and testing framework. Foundry provide } ``` +## LiFiDEXAggregator (LDA) Conventions + +The LiFiDEXAggregator (LDA) is a specialized component within the LI.FI ecosystem that provides efficient, modular DEX integration capabilities through its own Diamond Standard implementation. + +### Architecture Overview + +#### Core Components + +- **LiFiDEXAggregatorDiamond.sol**: Base EIP-2535 Diamond Proxy Contract for DEX Aggregator +- **CoreRouteFacet.sol**: Orchestrates route execution using direct function selector dispatch +- **BaseRouteConstants.sol**: Shared constants across DEX facets +- **PoolCallbackAuthenticator.sol**: Abstract contract providing pool callback authentication + +#### Location Structure + +``` +src/Periphery/LDA/ +├── LiFiDEXAggregatorDiamond.sol # LiFiDEXAggregatorDiamond Diamond proxy implementation +├── BaseRouteConstants.sol # Common constants for DEX facets +├── PoolCallbackAuthenticator.sol # Callback authentication base +├── Facets/ # LiFiDEXAggregator-specific facets +│ ├── CoreRouteFacet.sol # Route orchestration +│ ├── UniV3StyleFacet.sol # UniV3-style DEX integrations +│ ├── UniV2StyleFacet.sol # UniV2-style DEX integrations +│ ├── NativeWrapperFacet.sol # Native token wrapping +│ └── {DexName}Facet.sol # Custom DEX integrations +└── Errors/ + └── Errors.sol # LiFiDEXAggregator-specific error definitions +``` + +### DEX Integration Decision Tree + +When integrating a new DEX, follow this decision tree: + +1. **Is the DEX a UniV3 fork (same logic, different callback name)?** + - ✅ Yes → Extend `UniV3StyleFacet.sol` with a new callback + - Add tests inheriting `BaseUniV3StyleDEXFacetTest` + +2. **Else, is it a UniV2-style fork?** + - ✅ Yes → No new facet needed + - Add tests inheriting `BaseUniV2StyleDEXFacetTest` + +3. **Else, does it use a callback?** + - ✅ Yes → Create new custom facet with swap function and callback + - Write tests inheriting `BaseDEXFacetWithCallbackTest` + +4. **Else** → Create new custom facet with swap function without callbacks + - Write tests inheriting `BaseDEXFacetTest` + +### LiFiDEXAggregator Facet Requirements + +#### Naming and Location + +- Must reside in `src/Periphery/LDA/Facets/` +- Names must include "Facet" suffix +- Use descriptive names (e.g., `UniV3StyleFacet`, `CurveFacet`) + +#### Required Inheritance + +- **BaseRouteConstants**: For common constants (`DIRECTION_TOKEN0_TO_TOKEN1`, `FUNDS_IN_RECEIVER`) +- **PoolCallbackAuthenticator**: For facets requiring callback verification +- **No ReentrancyGuard**: CoreRouteFacet handles reentrancy protection + +#### Function Patterns + +**Swap Functions:** +- Must follow pattern: `swap{DexName}(bytes memory swapData, address from, address tokenIn, uint256 amountIn)` +- Use `LibPackedStream` for efficient parameter unpacking +- Handle token transfers based on `from` parameter (if `from == msg.sender`, pull tokens) + +**Callback Functions:** +- Must use `onlyExpectedPool` modifier +- **IMPORTANT**: Callback function names are protocol-specific and cannot be guessed. You must inspect the target pool contract's interface directly in the block explorer or source code to determine the exact callback function name(s). Examples include `uniswapV3SwapCallback`, `pancakeV3SwapCallback`, `swapX2YCallback`, `swapY2XCallback`, etc. +- Use `LibCallbackAuthenticator` for pool verification + +#### Parameter Handling + +- **swapData Encoding**: Use packed encoding for efficiency + - Common pattern: `[pool, direction, destinationAddress]` + - Additional parameters as needed per DEX +- **Direction Parameter**: Use `uint8` where `1 = token0 -> token1`, `0 = token1 -> token0` +- **Validation**: Always validate pool addresses and amounts. For invalid inputs, revert with `InvalidCallData()` from `GenericErrors.sol` + +#### Error Handling + +- **LiFiDEXAggregator-specific errors**: Define in `src/Periphery/LDA/LiFiDEXAggregatorErrors.sol` +- **Generic errors**: Use existing errors from `src/Errors/GenericErrors.sol` + +### LiFiDEXAggregator Testing Conventions + +#### Test File Structure + +``` +test/solidity/Periphery/LDA/ +├── BaseCoreRoute.t.sol # Base route testing functionality +├── BaseDEXFacet.t.sol # Base for custom DEX tests +├── BaseDEXFacetWithCallback.t.sol # Base for callback-enabled DEX tests +├── BaseUniV3StyleDEXFacet.t.sol # Base for UniV3-style DEX tests +├── BaseUniV2StyleDEXFacet.t.sol # Base for UniV2-style DEX tests +└── Facets/ + └── {DexName}Facet.t.sol # Specific DEX implementation tests +``` + +#### Test Implementation Requirements + +**All LDA DEX tests must implement:** + +1. **`_setupForkConfig()`**: Configure network and block number + - Use valid `networkName` from `config/networks.json` + - **IMPORTANT**: You must manually specify a block number where the target pools have healthy liquidity. Do not guess block numbers - check the pool's transaction history on a block explorer to find a recent block with sufficient reserves for testing + - Example: + ```solidity + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "mainnet", + blockNumber: 18500000 // Manually verified block with healthy pool liquidity + }); + } + ``` + +2. **`_createFacetAndSelectors()`**: Deploy facet and return selectors + - Return facet address and function selectors for diamond cut + - Include both swap function and callback selectors (if applicable) + +3. **`_setFacetInstance()`**: Connect test handle to diamond proxy + - Set local facet instance to diamond address after cut + +4. **`_setupDexEnv()`**: Configure test tokens and pools + - Set `tokenIn`, `tokenOut`, `poolInOut` with sufficient liquidity + - Verify pools exist and have proper reserves + +#### Test Categories by Inheritance + +**BaseDEXFacetTest** (Custom DEX without callbacks): +- Implement single-hop and multi-hop tests (if supported) +- Focus on direct swap execution +- Example: `SyncSwapV2Facet.t.sol` + +**BaseDEXFacetWithCallbackTest** (Custom DEX with callbacks): +- Include callback verification tests +- Override `_getCallbackSelector()` for negative tests +- Implement `_deployNoCallbackPool()` if needed +- Example: `IzumiV3Facet.t.sol` + +**BaseUniV3StyleDEXFacetTest** (UniV3 forks): +- Override `_getCallbackSelector()` for DEX-specific callback +- No multi-hop support (skipped for UniV3-style) +- Example: `AlgebraFacet.t.sol` + +**BaseUniV2StyleDEXFacetTest** (UniV2 forks): +- Override `_getPoolFee()` for DEX-specific fee structure +- Support multi-hop routing +- Example: `VelodromeV2Facet.t.sol` + +#### Test Validation Requirements + +- **Liquidity Validation**: Ensure pools have sufficient liquidity for test amounts +- **Token Decimals**: Override `_getDefaultAmountForTokenIn()` for non-18 decimal tokens +- **Pool Verification**: Verify pool addresses exist and are correctly configured. Ensure the chosen fork block number has the pools deployed and contains sufficient liquidity for testing + +### LiFiDEXAggregator Deployment Scripts + +#### Location and Naming + +- **Location**: `script/deploy/facets/LDA/` +- **Naming**: `Deploy{DexName}Facet.s.sol` +- **Pattern**: Follow standard deployment script structure + +#### Deployment Script Structure + +```solidity +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; +import { {DexName}Facet } from "lifi/Periphery/LDA/Facets/{DexName}Facet.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("{DexName}Facet") {} + + function run() public returns ({DexName}Facet deployed) { + deployed = {DexName}Facet(deploy(type({DexName}Facet).creationCode)); + } +} +``` ## Solidity Test Conventions (.t.sol files) ### File Naming and Structure diff --git a/deployments/_deployments_log_file.json b/deployments/_deployments_log_file.json index ad2991894..d764b4843 100644 --- a/deployments/_deployments_log_file.json +++ b/deployments/_deployments_log_file.json @@ -334,6 +334,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x623e85b8Bb1bCb8820e92BF889b3f725e15B6b0c", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:10:56", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "cronos": { @@ -483,6 +496,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x60AdE68e9a048e41FAbfe3e0c4738A4f332372BB", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-10 23:11:02", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345102", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "1.5.11" + } + ] } }, "base": { @@ -761,6 +787,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x85bc11e06719Ec8dDc3253676283F44E625598Ab", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:03:48", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "sonic": { @@ -1038,6 +1077,19 @@ "VERIFIED": "false" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x440Fd10949EC42Bd1034Fe7E54D742454FD9ED55", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:12:00", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] } }, "katana": { @@ -1066,6 +1118,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x1590b7fe02b49206Ee43a53Ac2641487cbE79976", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:18:13", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "vana": { @@ -1534,6 +1599,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x6a540600082339a0F1Be494f71e22647CE8F5c65", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:13:39", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "velas": { @@ -1669,6 +1747,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x602FA7c37f3fe532450F7D9039b17dC13df60a65", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-10 23:18:05", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345102", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "1.5.11" + } + ] } }, "base": { @@ -1934,6 +2025,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xE941EE035157Ca706A82AEcC53BfAcC5abBaa12D", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:06:05", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "sonic": { @@ -2212,6 +2316,19 @@ "VERIFIED": "false" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x2bAd54B70e8b7AC6829791de525B6fEe52DB1dBc", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:16:35", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] } }, "katana": { @@ -2240,6 +2357,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xD8d8b071A40db11D6479A6da25C5E8207074CfC3", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:19:16", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "vana": { @@ -2707,6 +2837,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x2aA310807442489ace0BE42E3E5607780722375c", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:17:44", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "velas": { @@ -2842,6 +2985,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x7A7d3d5caA0010a945C01ce75e4758697f841dcF", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-10 23:22:23", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345102", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "1.5.11" + } + ] } }, "base": { @@ -3121,6 +3277,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x56db197b1d1021ec1825a9A434049AE5d722bfad", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:09:59", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "sonic": { @@ -3385,6 +3554,19 @@ "VERIFIED": "false" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x4D57Be68eA18e441B16A12361195759c91dfB6aE", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:20:42", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] } }, "katana": { @@ -3413,6 +3595,19 @@ "VERIFIED": "true" } ] + }, + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x5ADA0122fBF4EA6f46320946469354D43d5D31e1", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:20:03", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] } }, "vana": { @@ -40526,6 +40721,17 @@ "SALT": "", "VERIFIED": "true" } + ], + "1.0.1": [ + { + "ADDRESS": "0xaA1E88f4D0cb0a798f1FeBAfc8fAb4778629D4e7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-09 00:51:30", + "CONSTRUCTOR_ARGS": "0x00000000000000000000000079001a5e762f3befc8e5871b42f6734e00498920", + "SALT": "22345117", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } ] }, "production": { @@ -40624,11 +40830,11 @@ ], "1.1.0": [ { - "ADDRESS": "0x36e1375B0755162d720276dFF6893DF02bd49225", + "ADDRESS": "0x33EcEb68994E0499a61FAda3b49Ab243e63555F1", "OPTIMIZER_RUNS": "1000000", - "TIMESTAMP": "2025-08-01 12:50:40", + "TIMESTAMP": "2025-09-08 22:27:21", "CONSTRUCTOR_ARGS": "0x000000000000000000000000023fa838682c115c2cfba96ef3791cb5bd931fc7", - "SALT": "", + "SALT": "22345119", "VERIFIED": "true", "ZK_SOLC_VERSION": "" } @@ -40769,6 +40975,19 @@ "ZK_SOLC_VERSION": "" } ] + }, + "staging": { + "1.1.0": [ + { + "ADDRESS": "0x33EcEb68994E0499a61FAda3b49Ab243e63555F1", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-08 22:22:45", + "CONSTRUCTOR_ARGS": "0x000000000000000000000000023fa838682c115c2cfba96ef3791cb5bd931fc7", + "SALT": "22345119", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] } }, "linea": { @@ -45708,5 +45927,1272 @@ ] } } + }, + "CoreRouteFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x0d47366d1Fbe6BC60B9633589EbEFDC8A5403bcC", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-12 15:10:45", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345115", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x07497D2C0B32537B0162dc35a4794F4B089a53Aa", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:20:08", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xCaC140589393C15DA1C8E45942B3b8228CBA95C8", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 10:18:57", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "12345680", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xb1cA06bEc28471fcA197a09494249c66929E37bC", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:42:37", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x36f16Baf237bAa4f6fe91ea392ddB74f67fE46D5", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:39:44", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x0745C26fABf7CC09D216b85Fd0e3E4F3a2D92722", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:23:21", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x8297890dfF469F90E10f0042E4f5ceB8e56903da", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:21:46", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x3598833FA88c04f4e06Fe41D44b71402f5832660", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:37:17", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "UniV3StyleFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x8D86c444698760F8bd4FFd2828B3fEA6259062F3", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 01:30:26", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xbE76705E06154dAb3A95837166ef04d890bDeA15", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:13:26", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x2AA70A1115F9bA6008bD04511AAF7d81E5251d21", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:30:56", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x6212d5d99379fa8678ca5C2Ad2a676097E7Ed037", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:59:13", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x2c9d60D7A57171e0eaAAE38d200801e8e5C91274", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:54:45", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x3a82c34B1D51C79beaC85A1db04873C8a1cc7FeA", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:34:49", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xE786a27820725884880306094FEdE01C7d2B9F5b", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:26:25", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x75D734DD826Aa126178B1AC5920FE7EF61252FF6", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:46:13", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "UniV2StyleFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xfDE53F231c0FC0430fDb88a6c834E8766c1597C4", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 01:12:08", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x181a353054883D9DdE6864Ba074226E5b77cf511", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:13:01", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x8ee5946F6d0818557cc49A862470D89A34b986d0", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:29:24", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x6741Dc1295Df351C9B26210026563cEA04c657bd", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:56:05", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x11Bb8fCeEf956201353257292f66A1a726c414b6", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:49:48", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x0D8332Fa8d5845D68311E106a55fc264e209f9E7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:33:10", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xdc592beC201b7C28cD5C60DEeaBA44D8ACdA61b7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:25:46", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xf0fcd6a8db420D5DEB48F84adDF89d783e13927f", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:44:58", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "NativeWrapperFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xE1914A712c15cA52e884fb82d6634229F04D9C1e", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 00:50:06", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x59A1514CD90a4c3662b3003450C8878448E6D6dD", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:12:11", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xd13d6DF1ef271dC887Df5499e4059e1AeC42Ea11", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:26:17", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xcC01769a3dF6F340f5CF0C7BB8a45C2d57BbA15b", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:51:33", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x9CcC15B2Acdf4929C6b343673ED94cdf13e9E59C", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:41:32", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xB3a006e66523275685BE0f4FC969A2928B4BF5cc", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:29:49", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x61d83FD6A30386024e33d66862e9627d53Ddf081", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:24:26", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xb08d2084955370ba83452a55c4b79Fb895852599", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:42:16", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "IzumiV3Facet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x462e4F563D75710216BBf658542D1561b3CB3142", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 00:39:43", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x7a7D7101a9A56882b34C0AC06328367f2356BB41", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:11:22", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x0e5898bAa8CAf208292a9f7b0C5DA7BbDFc95F4b", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-07 12:00:18", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345120", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x14d94db24A2ac5dEF684e44e618Aa9052371Feb1", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:47:10", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xFB29877e1ed744bCccfaAD0B55A3d6d1Ec244B45", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:33:24", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x6eE2187aA149aEE3C93b094A68Ce510E0736b3CD", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:26:35", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xC807C36D4094395818F432A557ea2e09cd4a5e93", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:23:04", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xd56c60D32fC9343Ff75386498940BA6d6bE71E92", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:39:41", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "LiFiDEXAggregatorDiamond": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x897e12b5f25187648561A2e719e2ad22125626Ca", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 21:54:15", + "CONSTRUCTOR_ARGS": "0x0000000000000000000000002b2c52b1b63c4bfc7f1a310a1734641d8e34de62000000000000000000000000b2a8517734cdf985d53f303a1f7759a34fdc772f", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x897e12b5f25187648561A2e719e2ad22125626Ca", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:09:40", + "CONSTRUCTOR_ARGS": "0x0000000000000000000000002b2c52b1b63c4bfc7f1a310a1734641d8e34de62000000000000000000000000b2a8517734cdf985d53f303a1f7759a34fdc772f", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x9257FA485C5714975247ca5C47A2cB0c4FA5A773", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:18:35", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "AlgebraFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x23453E98fB5bdB779780eeC434297873544d706C", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 00:25:59", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xfeE467d825052aa8347ee7Ed2D90D2568DA96EbF", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:10:11", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x5363c2db9eB0e9080574915b4C486B9851BA4326", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:18:35", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x041b54C9079E5289F63950A7d771a432F4E83035", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:40:27", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x50EFcBF64Cf45DE9bA8CB5B15aafB094Db402c53", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:35:30", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x7F7D2D2C8C58E8Ce5EF87AfE203C2931709a4Fe2", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:21:46", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xd61FaEDCdC9EeF3170AD2D06eC93b346e7a27f46", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:21:07", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x2215937Eda2Fb876f904BEFB78712E36369C61E7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:35:39", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "CurveFacet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x839b4B0aa0B68dE14814609C48067e4CBf559F67", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-12 15:11:06", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345115", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x02B8238bE17F05E9578D1F06876aE4BDc465dF7d", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:10:59", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x7B383eD28261835e63D2aed3b4A415B29438354B", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:21:41", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x854103F3b1bA069B86A9EFdb1e9796A873bCCf05", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:44:49", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x0CE83BcE19987935BfD9eb27FaF984ddc65B2fA4", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:43:51", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x29232CE04d6559E9Fa97c8CEda8CaB951eAbAfd7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:24:58", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xc0942d9DbE4D5bFbC80DF044AAd1341B68dDbBd3", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:22:25", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x8A3e0E1De58E73cA37273613F39211fF4d29DF3d", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:38:31", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "KatanaV3Facet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x522A52c5cae7d2740e68Eb5cDcbe997296f939E6", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 00:47:04", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xf33C1c24ccc5A137231d89272a2383c28B1dd046", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:11:45", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x4582E5095D17980D213c5Fb3959466C745593244", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-08 22:16:25", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345119", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xE5762D9874754a8F890A95031e1F99624EF5600E", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:49:20", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x5B36B428a05D6460Fc404D0c104D3Fc23cC98136", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:37:30", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x9dB2c7d7aEf3e0c5dEC9DF53903EBE4c30891e47", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:28:12", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x9C6e1543c127C98bAd9F25e3b59f49B0dcE0daEd", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:23:46", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x6F3e876F8701264996B176AD2df2f99D5B070aD6", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:41:01", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "SyncSwapV2Facet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x064813b9A1D501da2440a85686d6d367F19316Fa", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 00:58:41", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x283831120F19fd293206AB6FaEF1C45Cf83487D0", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:12:36", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xdFc21dcEc5220bfaB06D865312991F6058f5dfC6", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:27:50", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xd7078122Db3F81ac27f87FBC56BEC2E8ecB2539A", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 11:53:49", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x07616Aed6E7D346D9ef0154131e53A59d7646fDA", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:45:38", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x2D097d669Db40A7C64360CAE4e64e50485089170", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:31:27", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x60855F101E6D99204cC2B4aDC044E08efCEbD865", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:25:05", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x8028d853E44CaC441340e9b4fde6039604d8C9c2", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:43:35", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } + }, + "VelodromeV2Facet": { + "arbitrum": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x04BE6CC23d77038C1E0cd78c5c26962F958273Fd", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 01:43:39", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "optimism": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0xb516E77032DC56Ff21eeb3F18463b9a120E5C374", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-04 17:13:53", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "bsc": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x8CFB30c9A05357DAd207600A577f5251Fa7bCeab", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-06 19:32:31", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345121", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "apechain": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x696007104F48e7100be332314c6078E79bE4EAA1", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 12:01:33", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "ronin": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x71b9D7FFfa9F28f46351Ca0B8BeFA2744c89c74B", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 13:58:57", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "false", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "celo": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x5138DD64a101eaB616aA8B3D6C93A6658F117Cf0", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-09-26 14:36:29", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "24354312", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "plume": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x0583F88FEaAF3fDaEDB1331AAB87947BB518Aef9", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-01 00:27:08", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345109", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + }, + "zksync": { + "staging": { + "1.0.0": [ + { + "ADDRESS": "0x83A82B653B30A96F2f3DefDDB3E329Bc3dA9a6D7", + "OPTIMIZER_RUNS": "1000000", + "TIMESTAMP": "2025-10-13 20:47:34", + "CONSTRUCTOR_ARGS": "0x", + "SALT": "22345094", + "VERIFIED": "true", + "ZK_SOLC_VERSION": "" + } + ] + } + } } } diff --git a/deployments/apechain.diamond.lda.staging.json b/deployments/apechain.diamond.lda.staging.json new file mode 100644 index 000000000..27f476f99 --- /dev/null +++ b/deployments/apechain.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0x85bc11e06719Ec8dDc3253676283F44E625598Ab": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0xE941EE035157Ca706A82AEcC53BfAcC5abBaa12D": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x56db197b1d1021ec1825a9A434049AE5d722bfad": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0x041b54C9079E5289F63950A7d771a432F4E83035": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0xb1cA06bEc28471fcA197a09494249c66929E37bC": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0x854103F3b1bA069B86A9EFdb1e9796A873bCCf05": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0x14d94db24A2ac5dEF684e44e618Aa9052371Feb1": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0xE5762D9874754a8F890A95031e1F99624EF5600E": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0xcC01769a3dF6F340f5CF0C7BB8a45C2d57BbA15b": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0xd7078122Db3F81ac27f87FBC56BEC2E8ecB2539A": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0x6741Dc1295Df351C9B26210026563cEA04c657bd": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0x6212d5d99379fa8678ca5C2Ad2a676097E7Ed037": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0x696007104F48e7100be332314c6078E79bE4EAA1": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/apechain.staging.json b/deployments/apechain.staging.json index 83545a0f5..5bba8002d 100644 --- a/deployments/apechain.staging.json +++ b/deployments/apechain.staging.json @@ -1,3 +1,17 @@ { - "LiFiDEXAggregator": "0x31b0b4DF8fE055532bd106b2E7C8afCFd39B65De" + "LiFiDEXAggregator": "0x31b0b4DF8fE055532bd106b2E7C8afCFd39B65De", + "DiamondCutFacet": "0x85bc11e06719Ec8dDc3253676283F44E625598Ab", + "DiamondLoupeFacet": "0xE941EE035157Ca706A82AEcC53BfAcC5abBaa12D", + "OwnershipFacet": "0x56db197b1d1021ec1825a9A434049AE5d722bfad", + "LiFiDEXAggregatorDiamond": "0xAddC2ae2CAc0d457b41E746467e81c4E96964a61", + "AlgebraFacet": "0x041b54C9079E5289F63950A7d771a432F4E83035", + "CoreRouteFacet": "0xb1cA06bEc28471fcA197a09494249c66929E37bC", + "CurveFacet": "0x854103F3b1bA069B86A9EFdb1e9796A873bCCf05", + "IzumiV3Facet": "0x14d94db24A2ac5dEF684e44e618Aa9052371Feb1", + "KatanaV3Facet": "0xE5762D9874754a8F890A95031e1F99624EF5600E", + "NativeWrapperFacet": "0xcC01769a3dF6F340f5CF0C7BB8a45C2d57BbA15b", + "SyncSwapV2Facet": "0xd7078122Db3F81ac27f87FBC56BEC2E8ecB2539A", + "UniV2StyleFacet": "0x6741Dc1295Df351C9B26210026563cEA04c657bd", + "UniV3StyleFacet": "0x6212d5d99379fa8678ca5C2Ad2a676097E7Ed037", + "VelodromeV2Facet": "0x696007104F48e7100be332314c6078E79bE4EAA1" } \ No newline at end of file diff --git a/deployments/arbitrum.diamond.lda.staging.json b/deployments/arbitrum.diamond.lda.staging.json new file mode 100644 index 000000000..dd40c090a --- /dev/null +++ b/deployments/arbitrum.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0xB2A8517734CDf985d53F303A1f7759A34fdC772F": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0x3B75025167b5fEc275266F43c132709d58329cf4": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x050cd05AFf1367e6DC4c20dA96fFD31FCB39e219": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0xC02fBdb4a97f14E73F456aE77a603F85C1a5fD67": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0x0d47366d1Fbe6BC60B9633589EbEFDC8A5403bcC": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0x839b4B0aa0B68dE14814609C48067e4CBf559F67": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0x62cb24d3D3847af24b82D087E82bebC3305d16c6": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0x475d3f49A51f8ff44d84257C46Ee389333B1BF42": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0x32Dde41a556Ad153371FcA83e11723Bb7682C61d": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0xB99d4DBb0A6C6cdeBb1Df59e243600A0795a1055": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0x29999754A40dc303C1aC0bEF058658DE6098fbB7": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0x8162d12A6Db97C601F16BD474670DC8bF2456a41": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0x4f5EbfA073440800722DbaD2A57805ec3065C93e": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/arbitrum.diamond.staging.json b/deployments/arbitrum.diamond.staging.json index c905cba70..5a5519f35 100644 --- a/deployments/arbitrum.diamond.staging.json +++ b/deployments/arbitrum.diamond.staging.json @@ -125,9 +125,9 @@ "Name": "MayanFacet", "Version": "1.2.0" }, - "0x681a3409c35F12224c436D50Ce14F25f954B6Ea2": { - "Name": "RelayFacet", - "Version": "1.0.0" + "0xdaD5da5FB53EAe15f490Fb31F4573e56277b59CA": { + "Name": "", + "Version": "" }, "0x7C27b0FD92dbC5a1cA268255A649320E8C649e70": { "Name": "GasZipFacet", @@ -149,9 +149,9 @@ "Name": "GlacisFacet", "Version": "1.0.0" }, - "0xa884c21873A671bD010567cf97c937b153F842Cc": { + "0xaA1E88f4D0cb0a798f1FeBAfc8fAb4778629D4e7": { "Name": "ChainflipFacet", - "Version": "1.0.0" + "Version": "1.0.1" }, "0x371E61d9DC497C506837DFA47B8dccEF1da30459": { "Name": "PioneerFacet", @@ -173,7 +173,7 @@ "Name": "AcrossFacetPackedV4", "Version": "1.0.0" }, - "0x36e1375B0755162d720276dFF6893DF02bd49225": { + "0x33EcEb68994E0499a61FAda3b49Ab243e63555F1": { "Name": "GlacisFacet", "Version": "1.1.0" }, @@ -188,7 +188,6 @@ "FeeCollector": "0x7F8E9bEBd1Dea263A36a6916B99bd84405B9654a", "GasZipPeriphery": "", "LidoWrapper": "", - "LiFiDEXAggregator": "", "Patcher": "0x3971A968c03cd9640239C937F8d30D024840E691", "Permit2Proxy": "0xb33Fe241BEd9bf5F694101D7498F63a0d060F999", "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", diff --git a/deployments/arbitrum.staging.json b/deployments/arbitrum.staging.json index 74badba41..5cc915695 100644 --- a/deployments/arbitrum.staging.json +++ b/deployments/arbitrum.staging.json @@ -51,15 +51,26 @@ "ReceiverAcrossV3": "0xe4F3DEF14D61e47c696374453CD64d438FD277F8", "AcrossFacetPackedV3": "0x21767081Ff52CE5563A29f27149D01C7127775A2", "RelayFacet": "0x681a3409c35F12224c436D50Ce14F25f954B6Ea2", - "GlacisFacet": "0x36e1375B0755162d720276dFF6893DF02bd49225", + "GlacisFacet": "0x33EcEb68994E0499a61FAda3b49Ab243e63555F1", "PioneerFacet": "0x371E61d9DC497C506837DFA47B8dccEF1da30459", "GasZipFacet": "0x7C27b0FD92dbC5a1cA268255A649320E8C649e70", - "ChainflipFacet": "0xa884c21873A671bD010567cf97c937b153F842Cc", + "ChainflipFacet": "0xaA1E88f4D0cb0a798f1FeBAfc8fAb4778629D4e7", "WhitelistManagerFacet": "0xa574407A8bA37E83841eCa2084b1DAC51d0f0b8A", "LiFiDEXAggregator": "0x14aB08312a1EA45F76fd83AaE89A3118537FC06D", "AcrossFacetV4": "0xb1b7786dfbb59dB5d1a65d4Be6c92C3B112fFb9b", "ReceiverAcrossV4": "0x8fd9e1893fdE2e52b49F2875E63277Ba5675D014", "AcrossFacetPackedV4": "0xf536ed5A4310455FF39dBf90336e17d11550E7b4", "Patcher": "0x18069208cA7c2D55aa0073E047dD45587B26F6D4", - "RelayDepositoryFacet": "0x004E291b9244C811B0BE00cA2C179d54FAA5073D" + "RelayDepositoryFacet": "0x004E291b9244C811B0BE00cA2C179d54FAA5073D", + "LiFiDEXAggregatorDiamond": "0xF5f53304F9E82bEaBe3eE99cc12a3AB9990557de", + "AlgebraFacet": "0x23453E98fB5bdB779780eeC434297873544d706C", + "CoreRouteFacet": "0x0d47366d1Fbe6BC60B9633589EbEFDC8A5403bcC", + "CurveFacet": "0x839b4B0aa0B68dE14814609C48067e4CBf559F67", + "KatanaV3Facet": "0x522A52c5cae7d2740e68Eb5cDcbe997296f939E6", + "NativeWrapperFacet": "0xE1914A712c15cA52e884fb82d6634229F04D9C1e", + "SyncSwapV2Facet": "0x064813b9A1D501da2440a85686d6d367F19316Fa", + "UniV2StyleFacet": "0xfDE53F231c0FC0430fDb88a6c834E8766c1597C4", + "UniV3StyleFacet": "0x8D86c444698760F8bd4FFd2828B3fEA6259062F3", + "VelodromeV2Facet": "0x04BE6CC23d77038C1E0cd78c5c26962F958273Fd", + "IzumiV3Facet": "0x462e4F563D75710216BBf658542D1561b3CB3142" } diff --git a/deployments/bsc.diamond.lda.staging.json b/deployments/bsc.diamond.lda.staging.json new file mode 100644 index 000000000..3b16a040f --- /dev/null +++ b/deployments/bsc.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0x06045F5FA6EA7c6AcEb104b55BcD6C3dE3a08831": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0x8938CEa23C3c5eAABb895765f5B0b2b07D680402": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x53d4Bcd5BEa4e863376b7eA43D7465351a4d71B0": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0x5363c2db9eB0e9080574915b4C486B9851BA4326": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0x07497D2C0B32537B0162dc35a4794F4B089a53Aa": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0x7B383eD28261835e63D2aed3b4A415B29438354B": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0x0e5898bAa8CAf208292a9f7b0C5DA7BbDFc95F4b": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0x4582E5095D17980D213c5Fb3959466C745593244": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0xd13d6DF1ef271dC887Df5499e4059e1AeC42Ea11": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0xdFc21dcEc5220bfaB06D865312991F6058f5dfC6": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0x8ee5946F6d0818557cc49A862470D89A34b986d0": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0x2AA70A1115F9bA6008bD04511AAF7d81E5251d21": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0x8CFB30c9A05357DAd207600A577f5251Fa7bCeab": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/bsc.diamond.staging.json b/deployments/bsc.diamond.staging.json index 5ed969efa..73d375541 100644 --- a/deployments/bsc.diamond.staging.json +++ b/deployments/bsc.diamond.staging.json @@ -48,10 +48,6 @@ "0xA759CCd09b263b6348c570Daa91C7Ff713e03965": { "Name": "WithdrawFacet", "Version": "1.0.0" - }, - "0x610808987E1da09a8F82a30084Cc176c51f66aAB": { - "Name": "WhitelistManagerFacet", - "Version": "1.0.0" } }, "Periphery": { @@ -68,7 +64,7 @@ "ReceiverAcrossV4": "", "ReceiverChainflip": "", "ReceiverStargateV2": "", - "TokenWrapper": "" + "TokenWrapper": "0x5215E9fd223BC909083fbdB2860213873046e45d" } } } \ No newline at end of file diff --git a/deployments/bsc.staging.json b/deployments/bsc.staging.json index 915e589a9..1a58c6c85 100644 --- a/deployments/bsc.staging.json +++ b/deployments/bsc.staging.json @@ -34,6 +34,17 @@ "StargateFacetV2": "0x089153117bffd37CBbE0c604dAE8e493D4743fA8", "LiFiDEXAggregator": "0x6140b987d6b51fd75b66c3b07733beb5167c42fc", "GasZipPeriphery": "0x46d8Aa20D5aD98927Cf885De9eBf9436E8E551c2", - "CalldataVerificationFacet": "0x6f94e6760DA419A0243d046660ccF34309a6dBD3", - "WhitelistManagerFacet": "0x610808987E1da09a8F82a30084Cc176c51f66aAB" + "LiFiDEXAggregatorDiamond": "0x9257FA485C5714975247ca5C47A2cB0c4FA5A773", + "AlgebraFacet": "0x5363c2db9eB0e9080574915b4C486B9851BA4326", + "CoreRouteFacet": "0x07497D2C0B32537B0162dc35a4794F4B089a53Aa", + "CurveFacet": "0x7B383eD28261835e63D2aed3b4A415B29438354B", + "IzumiV3Facet": "0x0e5898bAa8CAf208292a9f7b0C5DA7BbDFc95F4b", + "KatanaV3Facet": "0x4582E5095D17980D213c5Fb3959466C745593244", + "NativeWrapperFacet": "0xd13d6DF1ef271dC887Df5499e4059e1AeC42Ea11", + "SyncSwapV2Facet": "0xdFc21dcEc5220bfaB06D865312991F6058f5dfC6", + "UniV2StyleFacet": "0x8ee5946F6d0818557cc49A862470D89A34b986d0", + "UniV3StyleFacet": "0x2AA70A1115F9bA6008bD04511AAF7d81E5251d21", + "VelodromeV2Facet": "0x8CFB30c9A05357DAd207600A577f5251Fa7bCeab", + "GlacisFacet": "0x33EcEb68994E0499a61FAda3b49Ab243e63555F1", + "CalldataVerificationFacet": "0x6f94e6760DA419A0243d046660ccF34309a6dBD3" } diff --git a/deployments/celo.diamond.lda.staging.json b/deployments/celo.diamond.lda.staging.json new file mode 100644 index 000000000..0634a614e --- /dev/null +++ b/deployments/celo.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0x623e85b8Bb1bCb8820e92BF889b3f725e15B6b0c": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0x6a540600082339a0F1Be494f71e22647CE8F5c65": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x2aA310807442489ace0BE42E3E5607780722375c": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0x7F7D2D2C8C58E8Ce5EF87AfE203C2931709a4Fe2": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0x0745C26fABf7CC09D216b85Fd0e3E4F3a2D92722": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0x29232CE04d6559E9Fa97c8CEda8CaB951eAbAfd7": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0x6eE2187aA149aEE3C93b094A68Ce510E0736b3CD": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0x9dB2c7d7aEf3e0c5dEC9DF53903EBE4c30891e47": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0xB3a006e66523275685BE0f4FC969A2928B4BF5cc": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0x2D097d669Db40A7C64360CAE4e64e50485089170": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0x0D8332Fa8d5845D68311E106a55fc264e209f9E7": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0x3a82c34B1D51C79beaC85A1db04873C8a1cc7FeA": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0x5138DD64a101eaB616aA8B3D6C93A6658F117Cf0": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/celo.staging.json b/deployments/celo.staging.json new file mode 100644 index 000000000..981877c8a --- /dev/null +++ b/deployments/celo.staging.json @@ -0,0 +1,16 @@ +{ + "DiamondCutFacet": "0x623e85b8Bb1bCb8820e92BF889b3f725e15B6b0c", + "DiamondLoupeFacet": "0x6a540600082339a0F1Be494f71e22647CE8F5c65", + "OwnershipFacet": "0x2aA310807442489ace0BE42E3E5607780722375c", + "LiFiDEXAggregatorDiamond": "0x8C2bee5ED1ec136401bF8139973FB45D06b366FD", + "AlgebraFacet": "0x7F7D2D2C8C58E8Ce5EF87AfE203C2931709a4Fe2", + "CoreRouteFacet": "0x0745C26fABf7CC09D216b85Fd0e3E4F3a2D92722", + "CurveFacet": "0x29232CE04d6559E9Fa97c8CEda8CaB951eAbAfd7", + "IzumiV3Facet": "0x6eE2187aA149aEE3C93b094A68Ce510E0736b3CD", + "KatanaV3Facet": "0x9dB2c7d7aEf3e0c5dEC9DF53903EBE4c30891e47", + "NativeWrapperFacet": "0xB3a006e66523275685BE0f4FC969A2928B4BF5cc", + "SyncSwapV2Facet": "0x2D097d669Db40A7C64360CAE4e64e50485089170", + "UniV2StyleFacet": "0x0D8332Fa8d5845D68311E106a55fc264e209f9E7", + "UniV3StyleFacet": "0x3a82c34B1D51C79beaC85A1db04873C8a1cc7FeA", + "VelodromeV2Facet": "0x5138DD64a101eaB616aA8B3D6C93A6658F117Cf0" +} \ No newline at end of file diff --git a/deployments/optimism.diamond.lda.staging.json b/deployments/optimism.diamond.lda.staging.json new file mode 100644 index 000000000..fb16d2ca1 --- /dev/null +++ b/deployments/optimism.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0xB2A8517734CDf985d53F303A1f7759A34fdC772F": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0x3B75025167b5fEc275266F43c132709d58329cf4": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x050cd05AFf1367e6DC4c20dA96fFD31FCB39e219": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0xfeE467d825052aa8347ee7Ed2D90D2568DA96EbF": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0xCaC140589393C15DA1C8E45942B3b8228CBA95C8": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0x02B8238bE17F05E9578D1F06876aE4BDc465dF7d": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0x7a7D7101a9A56882b34C0AC06328367f2356BB41": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0xf33C1c24ccc5A137231d89272a2383c28B1dd046": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0x59A1514CD90a4c3662b3003450C8878448E6D6dD": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0x283831120F19fd293206AB6FaEF1C45Cf83487D0": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0x181a353054883D9DdE6864Ba074226E5b77cf511": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0xbE76705E06154dAb3A95837166ef04d890bDeA15": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0xb516E77032DC56Ff21eeb3F18463b9a120E5C374": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/optimism.diamond.staging.json b/deployments/optimism.diamond.staging.json index 8422d6c81..4d76e08b6 100644 --- a/deployments/optimism.diamond.staging.json +++ b/deployments/optimism.diamond.staging.json @@ -49,6 +49,32 @@ "Name": "WithdrawFacet", "Version": "1.0.0" }, + "0x14cc46348EC4877d7a70e0f3e228aDa34af34DF2": { + "Name": "CBridgeFacetPacked", + "Version": "1.0.3" + }, + "0x371E073f6A09DCBEE1D2Ac56E940F878a0Ba9DAE": { + "Name": "CelerCircleBridgeFacet", + "Version": "1.0.1" + }, + "0x2Af20933E5886aFe275c0EEE4A2e65daA4E8b169": { + "Name": "CelerIMFacetMutable", + "Version": "" + }, + "0x380157643592725677F165b67642448CDCAeE026": { + "Name": "HopFacet", + "Version": "2.0.0" + }, + "0xf82135385765f1324257ffF74489F16382EBBb8A": { + "Name": "HopFacetOptimized", + "Version": "2.0.0" + }, + "0x5C3550CFA82803fC7342d55d8F806a2EdE675288": { + "Name": "HopFacetPacked", + "Version": "1.0.6" + }, + "0x4C362E97dAcF097e0CA71309375a4C13932f6217": { + "Name": "HyphenFacet", "0xF8875Bee262eA56113257B4035ab545bef2C38b6": { "Name": "WhitelistManagerFacet", "Version": "1.0.0" @@ -56,6 +82,62 @@ "0x2A6DB28EEe8d006d4F0781b57af3B24603b65A70": { "Name": "WhitelistManagerFacet", "Version": "1.0.0" + }, + "0xa137Fe4C41A2E04ca34578DC9023ad45cC194389": { + "Name": "", + "Version": "" + }, + "0x6124C65B6264bE13f059b7C3A891a5b77DA8Bd95": { + "Name": "AcrossFacetV3", + "Version": "1.0.0" + }, + "0x4352459F6BE1C7D1278F8c34Bb598b0feeB50f8b": { + "Name": "AcrossFacetPackedV3", + "Version": "1.0.0" + }, + "0x3cf7dE0e31e13C93c8Aada774ADF1C7eD58157f5": { + "Name": "RelayFacet", + "Version": "1.0.0" + }, + "0x605Daee0FF23B5d678D47BF17037789115bF5750": { + "Name": "AcrossFacetV3", + "Version": "1.0.0" + }, + "0x7A7dA456a99B5C8fef3D3E3c032a9F13949AdF07": { + "Name": "AcrossFacetV3", + "Version": "1.1.0" + }, + "0x371E61d9DC497C506837DFA47B8dccEF1da30459": { + "Name": "PioneerFacet", + "Version": "1.0.0" + }, + "0xfEeCe7B3e68B9cBeADB60598973704a776ac3ca1": { + "Name": "GasZipFacet", + "Version": "2.0.4" + }, + "0x91559A75bd9045681265C77922b3cAeDB3D5120d": { + "Name": "AcrossFacetV4", + "Version": "1.0.0" + }, + "0xf536ed5A4310455FF39dBf90336e17d11550E7b4": { + "Name": "AcrossFacetPackedV4", + "Version": "1.0.0" + }, + "0x36e1375B0755162d720276dFF6893DF02bd49225": { + "Name": "GlacisFacet", + "Version": "1.1.0" + }, + "0x0f4D0D8890bAF9036D3895315559a6F0d573e2EC": { + "Name": "", + "Version": "" + }, + "0x59A1Bcaa32EdB1a233fEF945857529BBD6df247f": { + "Name": "MayanFacet", + "Version": "1.2.2" + }, + "0xc4884225aeFe7218f9f489A5Eb8beB504ab272AA": { + "Name": "", + "Version": "" } }, "Periphery": { @@ -76,3 +158,4 @@ } } } +} diff --git a/deployments/optimism.staging.json b/deployments/optimism.staging.json index a0ff627b5..c3f9fefaf 100644 --- a/deployments/optimism.staging.json +++ b/deployments/optimism.staging.json @@ -51,5 +51,16 @@ "AcrossFacetV4": "0x91559A75bd9045681265C77922b3cAeDB3D5120d", "ReceiverAcrossV4": "0x1d5bD612Ce761060A4bEd77b606ab7e723D4E91E", "AcrossFacetPackedV4": "0xf536ed5A4310455FF39dBf90336e17d11550E7b4", - "WhitelistManagerFacet": "0x2A6DB28EEe8d006d4F0781b57af3B24603b65A70" + "WhitelistManagerFacet": "0x2A6DB28EEe8d006d4F0781b57af3B24603b65A70", + "LiFiDEXAggregatorDiamond": "0x897e12b5f25187648561A2e719e2ad22125626Ca", + "AlgebraFacet": "0xfeE467d825052aa8347ee7Ed2D90D2568DA96EbF", + "CurveFacet": "0x02B8238bE17F05E9578D1F06876aE4BDc465dF7d", + "IzumiV3Facet": "0x7a7D7101a9A56882b34C0AC06328367f2356BB41", + "KatanaV3Facet": "0xf33C1c24ccc5A137231d89272a2383c28B1dd046", + "NativeWrapperFacet": "0x59A1514CD90a4c3662b3003450C8878448E6D6dD", + "SyncSwapV2Facet": "0x283831120F19fd293206AB6FaEF1C45Cf83487D0", + "UniV2StyleFacet": "0x181a353054883D9DdE6864Ba074226E5b77cf511", + "UniV3StyleFacet": "0xbE76705E06154dAb3A95837166ef04d890bDeA15", + "VelodromeV2Facet": "0xb516E77032DC56Ff21eeb3F18463b9a120E5C374", + "CoreRouteFacet": "0xCaC140589393C15DA1C8E45942B3b8228CBA95C8" } diff --git a/deployments/plume.diamond.lda.staging.json b/deployments/plume.diamond.lda.staging.json new file mode 100644 index 000000000..c97be9525 --- /dev/null +++ b/deployments/plume.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0x1590b7fe02b49206Ee43a53Ac2641487cbE79976": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0xD8d8b071A40db11D6479A6da25C5E8207074CfC3": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x5ADA0122fBF4EA6f46320946469354D43d5D31e1": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0xd61FaEDCdC9EeF3170AD2D06eC93b346e7a27f46": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0x8297890dfF469F90E10f0042E4f5ceB8e56903da": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0xc0942d9DbE4D5bFbC80DF044AAd1341B68dDbBd3": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0xC807C36D4094395818F432A557ea2e09cd4a5e93": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0x9C6e1543c127C98bAd9F25e3b59f49B0dcE0daEd": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0x61d83FD6A30386024e33d66862e9627d53Ddf081": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0x60855F101E6D99204cC2B4aDC044E08efCEbD865": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0xdc592beC201b7C28cD5C60DEeaBA44D8ACdA61b7": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0xE786a27820725884880306094FEdE01C7d2B9F5b": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0x0583F88FEaAF3fDaEDB1331AAB87947BB518Aef9": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/plume.staging.json b/deployments/plume.staging.json new file mode 100644 index 000000000..907b00363 --- /dev/null +++ b/deployments/plume.staging.json @@ -0,0 +1,16 @@ +{ + "DiamondCutFacet": "0x1590b7fe02b49206Ee43a53Ac2641487cbE79976", + "DiamondLoupeFacet": "0xD8d8b071A40db11D6479A6da25C5E8207074CfC3", + "OwnershipFacet": "0x5ADA0122fBF4EA6f46320946469354D43d5D31e1", + "LiFiDEXAggregatorDiamond": "0x5cFf03bc36C1dD2967b0d09Ac74C80F0e2D2612e", + "AlgebraFacet": "0xd61FaEDCdC9EeF3170AD2D06eC93b346e7a27f46", + "CoreRouteFacet": "0x8297890dfF469F90E10f0042E4f5ceB8e56903da", + "CurveFacet": "0xc0942d9DbE4D5bFbC80DF044AAd1341B68dDbBd3", + "IzumiV3Facet": "0xC807C36D4094395818F432A557ea2e09cd4a5e93", + "KatanaV3Facet": "0x9C6e1543c127C98bAd9F25e3b59f49B0dcE0daEd", + "NativeWrapperFacet": "0x61d83FD6A30386024e33d66862e9627d53Ddf081", + "SyncSwapV2Facet": "0x60855F101E6D99204cC2B4aDC044E08efCEbD865", + "UniV2StyleFacet": "0xdc592beC201b7C28cD5C60DEeaBA44D8ACdA61b7", + "UniV3StyleFacet": "0xE786a27820725884880306094FEdE01C7d2B9F5b", + "VelodromeV2Facet": "0x0583F88FEaAF3fDaEDB1331AAB87947BB518Aef9" +} diff --git a/deployments/ronin.diamond.lda.staging.json b/deployments/ronin.diamond.lda.staging.json new file mode 100644 index 000000000..4579f4d9e --- /dev/null +++ b/deployments/ronin.diamond.lda.staging.json @@ -0,0 +1,5 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": {} + } +} \ No newline at end of file diff --git a/deployments/ronin.staging.json b/deployments/ronin.staging.json new file mode 100644 index 000000000..1f0849c60 --- /dev/null +++ b/deployments/ronin.staging.json @@ -0,0 +1,16 @@ +{ + "DiamondCutFacet": "0x440Fd10949EC42Bd1034Fe7E54D742454FD9ED55", + "DiamondLoupeFacet": "0x2bAd54B70e8b7AC6829791de525B6fEe52DB1dBc", + "OwnershipFacet": "0x4D57Be68eA18e441B16A12361195759c91dfB6aE", + "LiFiDEXAggregatorDiamond": "0xb651Cc7006B60535d2287b9B283875530dCEd8B0", + "AlgebraFacet": "0x50EFcBF64Cf45DE9bA8CB5B15aafB094Db402c53", + "CoreRouteFacet": "0x36f16Baf237bAa4f6fe91ea392ddB74f67fE46D5", + "CurveFacet": "0x0CE83BcE19987935BfD9eb27FaF984ddc65B2fA4", + "IzumiV3Facet": "0xFB29877e1ed744bCccfaAD0B55A3d6d1Ec244B45", + "KatanaV3Facet": "0x5B36B428a05D6460Fc404D0c104D3Fc23cC98136", + "NativeWrapperFacet": "0x9CcC15B2Acdf4929C6b343673ED94cdf13e9E59C", + "SyncSwapV2Facet": "0x07616Aed6E7D346D9ef0154131e53A59d7646fDA", + "UniV2StyleFacet": "0x11Bb8fCeEf956201353257292f66A1a726c414b6", + "UniV3StyleFacet": "0x2c9d60D7A57171e0eaAAE38d200801e8e5C91274", + "VelodromeV2Facet": "0x71b9D7FFfa9F28f46351Ca0B8BeFA2744c89c74B" +} \ No newline at end of file diff --git a/deployments/zksync.diamond.lda.staging.json b/deployments/zksync.diamond.lda.staging.json new file mode 100644 index 000000000..dd230c301 --- /dev/null +++ b/deployments/zksync.diamond.lda.staging.json @@ -0,0 +1,58 @@ +{ + "LiFiDEXAggregatorDiamond": { + "Facets": { + "0x60AdE68e9a048e41FAbfe3e0c4738A4f332372BB": { + "Name": "DiamondCutFacet", + "Version": "1.0.0" + }, + "0x602FA7c37f3fe532450F7D9039b17dC13df60a65": { + "Name": "DiamondLoupeFacet", + "Version": "1.0.0" + }, + "0x7A7d3d5caA0010a945C01ce75e4758697f841dcF": { + "Name": "OwnershipFacet", + "Version": "1.0.0" + }, + "0x2215937Eda2Fb876f904BEFB78712E36369C61E7": { + "Name": "AlgebraFacet", + "Version": "1.0.0" + }, + "0x3598833FA88c04f4e06Fe41D44b71402f5832660": { + "Name": "CoreRouteFacet", + "Version": "1.0.0" + }, + "0x8A3e0E1De58E73cA37273613F39211fF4d29DF3d": { + "Name": "CurveFacet", + "Version": "1.0.0" + }, + "0xd56c60D32fC9343Ff75386498940BA6d6bE71E92": { + "Name": "IzumiV3Facet", + "Version": "1.0.0" + }, + "0x6F3e876F8701264996B176AD2df2f99D5B070aD6": { + "Name": "KatanaV3Facet", + "Version": "1.0.0" + }, + "0xb08d2084955370ba83452a55c4b79Fb895852599": { + "Name": "NativeWrapperFacet", + "Version": "1.0.0" + }, + "0x8028d853E44CaC441340e9b4fde6039604d8C9c2": { + "Name": "SyncSwapV2Facet", + "Version": "1.0.0" + }, + "0xf0fcd6a8db420D5DEB48F84adDF89d783e13927f": { + "Name": "UniV2StyleFacet", + "Version": "1.0.0" + }, + "0x75D734DD826Aa126178B1AC5920FE7EF61252FF6": { + "Name": "UniV3StyleFacet", + "Version": "1.0.0" + }, + "0x83A82B653B30A96F2f3DefDDB3E329Bc3dA9a6D7": { + "Name": "VelodromeV2Facet", + "Version": "1.0.0" + } + } + } +} \ No newline at end of file diff --git a/deployments/zksync.staging.json b/deployments/zksync.staging.json new file mode 100644 index 000000000..50f806732 --- /dev/null +++ b/deployments/zksync.staging.json @@ -0,0 +1,16 @@ +{ + "DiamondCutFacet": "0x60AdE68e9a048e41FAbfe3e0c4738A4f332372BB", + "DiamondLoupeFacet": "0x602FA7c37f3fe532450F7D9039b17dC13df60a65", + "OwnershipFacet": "0x7A7d3d5caA0010a945C01ce75e4758697f841dcF", + "LiFiDEXAggregatorDiamond": "0x1cE85531309F1F0FAaFe1a47dF1b5e90D0566834", + "AlgebraFacet": "0x2215937Eda2Fb876f904BEFB78712E36369C61E7", + "CoreRouteFacet": "0x3598833FA88c04f4e06Fe41D44b71402f5832660", + "CurveFacet": "0x8A3e0E1De58E73cA37273613F39211fF4d29DF3d", + "IzumiV3Facet": "0xd56c60D32fC9343Ff75386498940BA6d6bE71E92", + "KatanaV3Facet": "0x6F3e876F8701264996B176AD2df2f99D5B070aD6", + "NativeWrapperFacet": "0xb08d2084955370ba83452a55c4b79Fb895852599", + "SyncSwapV2Facet": "0x8028d853E44CaC441340e9b4fde6039604d8C9c2", + "UniV2StyleFacet": "0xf0fcd6a8db420D5DEB48F84adDF89d783e13927f", + "UniV3StyleFacet": "0x75D734DD826Aa126178B1AC5920FE7EF61252FF6", + "VelodromeV2Facet": "0x83A82B653B30A96F2f3DefDDB3E329Bc3dA9a6D7" +} diff --git a/script/demoScripts/utils/demoScriptHelpers.ts b/script/demoScripts/utils/demoScriptHelpers.ts index bde11a3ec..739bcb3db 100644 --- a/script/demoScripts/utils/demoScriptHelpers.ts +++ b/script/demoScripts/utils/demoScriptHelpers.ts @@ -612,12 +612,30 @@ const normalizePrivateKey = (pk: string): `0x${string}` => { } /** - * Return the correct RPC environment variable - * (e.g. `ETH_NODE_URI_ARBITRUM` or `ETH_NODE_URI_MAINNET`) + * Get RPC URL for a chain, with fallback from environment variable to networks.json + * @param chain - The supported chain name + * @returns RPC URL string */ -const getRpcUrl = (chain: SupportedChain) => { +export const getRpcUrl = (chain: SupportedChain): string => { + // First, try to get from environment variable const envKey = `ETH_NODE_URI_${chain.toUpperCase()}` - return getEnvVar(envKey) + const envRpcUrl = process.env[envKey] + + if (envRpcUrl) { + return envRpcUrl + } + + // Fallback to networks.json + const networksConfig = networks as Record + const networkRpcUrl = networksConfig[chain.toLowerCase()]?.rpcUrl + + if (networkRpcUrl) { + return networkRpcUrl + } + + throw new Error( + `No RPC URL found for chain '${chain}' in environment variable '${envKey}' or networks.json` + ) } /** diff --git a/script/deploy/deployAllContracts.sh b/script/deploy/deployAllContracts.sh index ccca65f85..1d3825134 100755 --- a/script/deploy/deployAllContracts.sh +++ b/script/deploy/deployAllContracts.sh @@ -183,7 +183,7 @@ deployAllContracts() { echo "" echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> now updating core facets in diamond contract" - diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$DIAMOND_CONTRACT_NAME" "UpdateCoreFacets" false + diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$DIAMOND_CONTRACT_NAME" "UpdateCoreFacets" # check if last command was executed successfully, otherwise exit script with error message checkFailure $? "update core facets in $DIAMOND_CONTRACT_NAME on network $NETWORK" diff --git a/script/deploy/deployAllLDAContracts.sh b/script/deploy/deployAllLDAContracts.sh new file mode 100644 index 000000000..8ba56cf92 --- /dev/null +++ b/script/deploy/deployAllLDAContracts.sh @@ -0,0 +1,277 @@ +#!/bin/bash + +# LiFi DEX Aggregator (LDA) full deployment script +# - checks prerequisites of core facets (defined in ldaCoreFacets in global.json) +# - deploys LDA diamond contract +# - adds core facets to LDA diamond +# - deploys and adds LDA-specific facets +# - creates LDA deployment logs +# - runs health check +# - transfers ownership to multisig (production only) + +deployAllLDAContracts() { + echo "[info] =====================================================================" + echo "[info] Starting LiFi DEX Aggregator (LDA) Diamond deployment" + echo "[info] =====================================================================" + + # load required resources + source script/config.sh + source script/helperFunctions.sh + source script/deploy/deployFacetAndAddToDiamond.sh + source script/tasks/diamondUpdateFacet.sh + + # read function arguments into variables + local NETWORK="$1" + local ENVIRONMENT="$2" + + # load env variables + source .env + + # get file suffix based on value in variable ENVIRONMENT + local FILE_SUFFIX=$(getFileSuffix "$ENVIRONMENT") + + # logging for debug purposes + echo "" + echoDebug "in function deployAllLDAContracts" + echoDebug "NETWORK=$NETWORK" + echoDebug "ENVIRONMENT=$ENVIRONMENT" + echoDebug "FILE_SUFFIX=$FILE_SUFFIX" + echo "" + + # LDA Diamond contract name + local LDA_DIAMOND_CONTRACT_NAME="LiFiDEXAggregatorDiamond" + + # ========================================================================= + # STEP 1: Check prerequisites - ensure core facets are deployed + # ========================================================================= + echo "" + echo "[info] STEP 1: Checking LDA core facets availability..." + + # Check if regular network deployment file exists (where core facets should be) + local REGULAR_DEPLOYMENT_FILE="./deployments/${NETWORK}.${FILE_SUFFIX}json" + if [[ ! -f "$REGULAR_DEPLOYMENT_FILE" ]]; then + echo "" + echo "[ERROR] ❌ LiFi DEX Aggregator (LDA) Diamond deployment failed!" + echo "[ERROR] Regular network deployment file not found: $REGULAR_DEPLOYMENT_FILE" + echo "[ERROR] LDA requires core facets from the regular LiFi Diamond deployment." + echo "[ERROR] Please deploy the regular LiFi Diamond first using option 3 in the script master menu." + echo "" + return 1 + fi + + # Read LDA core facets from global.json config file + local GLOBAL_CONFIG_PATH="./config/global.json" + if [[ ! -f "$GLOBAL_CONFIG_PATH" ]]; then + error "Global config file not found: $GLOBAL_CONFIG_PATH" + return 1 + fi + + # Extract LDA core facets from JSON config + local LDA_CORE_FACETS_JSON=$(jq -r '.ldaCoreFacets[]' "$GLOBAL_CONFIG_PATH") + local LDA_CORE_FACETS=() + while IFS= read -r facet; do + LDA_CORE_FACETS+=("$facet") + done <<< "$LDA_CORE_FACETS_JSON" + + echo "[info] Checking for ${#LDA_CORE_FACETS[@]} LDA core facets: ${LDA_CORE_FACETS[*]}" + + local MISSING_FACETS=() + + # Check each LDA core facet exists in regular deployment logs + for FACET_NAME in "${LDA_CORE_FACETS[@]}"; do + local FACET_ADDRESS=$(getContractAddressFromDeploymentLogs "$NETWORK" "$ENVIRONMENT" "$FACET_NAME") + + if [[ -z "$FACET_ADDRESS" ]]; then + echo "[error] ❌ LDA core facet $FACET_NAME not found" + MISSING_FACETS+=("$FACET_NAME") + else + echo "[info] ✅ LDA core facet $FACET_NAME found at: $FACET_ADDRESS" + fi + done + + # If any facets are missing, fail the deployment + if [[ ${#MISSING_FACETS[@]} -gt 0 ]]; then + echo "" + echo "[ERROR] ❌ LDA deployment failed!" + echo "[ERROR] Missing core facets: ${MISSING_FACETS[*]}" + echo "[ERROR] Please deploy the regular LiFi Diamond first." + echo "" + return 1 + fi + + echo "[info] ✅ All LDA core facets are available" + + # ========================================================================= + # STEP 2: Deploy LDA Diamond + # ========================================================================= + echo "" + echo "[info] STEP 2: Deploying LDA Diamond..." + + # get current LDA diamond contract version + local VERSION=$(getCurrentContractVersion "$LDA_DIAMOND_CONTRACT_NAME") + local BYTECODE=$(getBytecodeFromArtifact "$LDA_DIAMOND_CONTRACT_NAME") + local SALT_INPUT="$BYTECODE""$SALT" + local DEPLOYSALT=$(cast keccak "$SALT_INPUT") + + # Determine the correct deploy script path based on network type + if isZkEvmNetwork "$NETWORK"; then + local DEPLOY_SCRIPT_PATH="script/deploy/zksync/LDA/DeployLiFiDEXAggregatorDiamond.zksync.s.sol" + echo "[info] Deploying $LDA_DIAMOND_CONTRACT_NAME using ZkSync script..." + + # Compile contracts with ZkSync compiler first + echo "[info] Compiling contracts with ZkSync compiler..." + FOUNDRY_PROFILE=zksync ./foundry-zksync/forge build --zksync + checkFailure $? "compile contracts with ZkSync compiler" + + + # For ZkSync networks, use ZkSync-specific deployment with DEPLOYSALT and regular file suffix + local RAW_RETURN_DATA=$(FOUNDRY_PROFILE=zksync DEPLOYSALT=$DEPLOYSALT NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX DEFAULT_DIAMOND_ADDRESS_DEPLOYSALT=$DEFAULT_DIAMOND_ADDRESS_DEPLOYSALT DEPLOY_TO_DEFAULT_DIAMOND_ADDRESS=$DEPLOY_TO_DEFAULT_DIAMOND_ADDRESS PRIVATE_KEY=$(getPrivateKey "$NETWORK" "$ENVIRONMENT") DIAMOND_TYPE=$DIAMOND_TYPE ./foundry-zksync/forge script "$DEPLOY_SCRIPT_PATH" -f "$NETWORK" -vvvvv --json --broadcast --skip-simulation --slow --zksync --gas-estimate-multiplier "${GAS_ESTIMATE_MULTIPLIER:-130}") + else + local DEPLOY_SCRIPT_PATH="script/deploy/facets/LDA/DeployLiFiDEXAggregatorDiamond.s.sol" + + # Get required deployment variables for CREATE3 deployment + local CREATE3_FACTORY_ADDRESS=$(getCreate3FactoryAddress "$NETWORK") + + echo "[info] Deploying $LDA_DIAMOND_CONTRACT_NAME using CREATE3 factory..." + # Deploy the LDA diamond using CREATE3 factory + local RAW_RETURN_DATA=$(DEPLOYSALT=$DEPLOYSALT CREATE3_FACTORY_ADDRESS=$CREATE3_FACTORY_ADDRESS NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX DEFAULT_DIAMOND_ADDRESS_DEPLOYSALT=$DEFAULT_DIAMOND_ADDRESS_DEPLOYSALT DEPLOY_TO_DEFAULT_DIAMOND_ADDRESS=$DEPLOY_TO_DEFAULT_DIAMOND_ADDRESS PRIVATE_KEY=$(getPrivateKey "$NETWORK" "$ENVIRONMENT") DIAMOND_TYPE=$DIAMOND_TYPE forge script "$DEPLOY_SCRIPT_PATH" -f "$NETWORK" -vvvvv --json --broadcast --legacy --slow --gas-estimate-multiplier "${GAS_ESTIMATE_MULTIPLIER:-130}") + fi + + # Extract deployed address + local LDA_DIAMOND_ADDRESS=$(extractDeployedAddressFromRawReturnData "$RAW_RETURN_DATA" "$NETWORK") + if [[ -z "$LDA_DIAMOND_ADDRESS" || "$LDA_DIAMOND_ADDRESS" == "null" ]]; then + error "❌ Failed to deploy $LDA_DIAMOND_CONTRACT_NAME - could not extract address" + return 1 + fi + + echo "[info] ✅ $LDA_DIAMOND_CONTRACT_NAME deployed at: $LDA_DIAMOND_ADDRESS" + + # Save contract in regular network deployment file + saveContract "$NETWORK" "$LDA_DIAMOND_CONTRACT_NAME" "$LDA_DIAMOND_ADDRESS" "$FILE_SUFFIX" + + # ========================================================================= + # STEP 3: Add core facets to LDA Diamond + # ========================================================================= + echo "" + echo "[info] STEP 3: Adding core facets to LDA Diamond..." + + diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$LDA_DIAMOND_CONTRACT_NAME" "UpdateLDACoreFacets" + + if [ $? -ne 0 ]; then + error "❌ Failed to add core facets to LDA Diamond" + return 1 + fi + + echo "[info] ✅ Core facets added to LDA Diamond" + + # ========================================================================= + # STEP 4: Deploy and add LDA-specific facets + # ========================================================================= + echo "" + echo "[info] STEP 4: Deploying and adding LDA-specific facets..." + + # Get all LDA facet contract names from the LDA Facets directory + local LDA_FACETS_PATH="src/Periphery/LDA/Facets/" + echo "[info] Getting LDA facets from directory: $LDA_FACETS_PATH" + + # Deploy all non-core LDA facets and add to diamond + for FACET_NAME in $(getContractNamesInFolder "$LDA_FACETS_PATH"); do + echo "[info] Deploying and adding LDA facet: $FACET_NAME" + + # get current contract version + local FACET_VERSION=$(getCurrentContractVersion "$FACET_NAME") + + # deploy LDA facet and add to diamond using unified function + deployFacetAndAddToDiamond "$NETWORK" "$ENVIRONMENT" "$FACET_NAME" "$LDA_DIAMOND_CONTRACT_NAME" "$FACET_VERSION" + + # check if deployment was successful + if [ $? -eq 0 ]; then + echo "[info] ✅ LDA facet $FACET_NAME deployed and added" + else + error "❌ Failed to deploy LDA facet $FACET_NAME" + return 1 + fi + done + + echo "[info] ✅ All LDA facets deployed and added" + + # ========================================================================= + # STEP 5: Create LDA diamond deployment logs + # ========================================================================= + echo "" + echo "[info] STEP 5: Creating LDA diamond deployment logs..." + + # Update diamond logs + updateDiamondLogs "$ENVIRONMENT" "$NETWORK" + + if [ $? -ne 0 ]; then + error "❌ Failed to update LDA diamond logs" + return 1 + fi + + echo "[info] ✅ LDA diamond logs created" + + # ========================================================================= + # STEP 6: Run health check + # ========================================================================= + echo "" + echo "[info] STEP 6: Running LDA health check..." + + bun script/deploy/ldaHealthCheck.ts --network "$NETWORK" --environment "$ENVIRONMENT" + + if [ $? -ne 0 ]; then + warning "⚠️ LDA health check failed - please review the output above" + echo "Continuing with ownership transfer..." + else + echo "[info] ✅ LDA health check passed" + fi + + # ========================================================================= + # STEP 7: Transfer ownership to multisig (production only) + # ========================================================================= + if [[ "$ENVIRONMENT" == "production" ]]; then + echo "" + echo "[info] STEP 7: Transferring LDA Diamond ownership to multisig..." + + # Get SAFE address from networks.json + local SAFE_ADDRESS=$(jq -r ".${NETWORK}.safeAddress" "./config/networks.json") + if [[ -z "$SAFE_ADDRESS" || "$SAFE_ADDRESS" == "null" ]]; then + error "❌ SAFE address not found in networks.json for network $NETWORK" + return 1 + fi + + echo "[info] Transferring LDA Diamond ownership to multisig: $SAFE_ADDRESS" + + # Transfer ownership directly to multisig + cast send "$LDA_DIAMOND_ADDRESS" "transferOwnership(address)" "$SAFE_ADDRESS" --private-key "$(getPrivateKey "$NETWORK" "$ENVIRONMENT")" --rpc-url "$(getRPCUrl "$NETWORK")" --legacy + + if [ $? -eq 0 ]; then + echo "[info] ✅ LDA Diamond ownership transferred to multisig: $SAFE_ADDRESS" + else + error "❌ Failed to transfer LDA Diamond ownership" + return 1 + fi + else + echo "" + echo "[info] STEP 7: Skipping ownership transfer (staging environment)" + fi + + # ========================================================================= + # DEPLOYMENT COMPLETE + # ========================================================================= + echo "" + echo "[info] =====================================================================" + echo "[info] ✅ LiFi DEX Aggregator (LDA) Diamond deployment COMPLETE!" + echo "[info] =====================================================================" + echo "[info] LDA Diamond Address: $LDA_DIAMOND_ADDRESS" + echo "[info] Network: $NETWORK" + echo "[info] Environment: $ENVIRONMENT" + if [[ "$ENVIRONMENT" == "production" ]]; then + echo "[info] Owner: $SAFE_ADDRESS (multisig)" + else + echo "[info] Owner: $(getDeployerAddress "" "$ENVIRONMENT") (deployer)" + fi + echo "[info] =====================================================================" + + return 0 +} \ No newline at end of file diff --git a/script/deploy/deployFacetAndAddToDiamond.sh b/script/deploy/deployFacetAndAddToDiamond.sh index b46a4025a..67ace1c00 100755 --- a/script/deploy/deployFacetAndAddToDiamond.sh +++ b/script/deploy/deployFacetAndAddToDiamond.sh @@ -11,7 +11,6 @@ function deployFacetAndAddToDiamond() { source script/deploy/deploySingleContract.sh source script/tasks/diamondUpdatePeriphery.sh - # read function arguments into variables local NETWORK="$1" local ENVIRONMENT="$2" @@ -73,12 +72,13 @@ function deployFacetAndAddToDiamond() { echo "[info] selected diamond type: $DIAMOND_CONTRACT_NAME" fi - # get diamond address from deployments script - local DIAMOND_ADDRESS=$(jq -r '.'"$DIAMOND_CONTRACT_NAME" "./deployments/${NETWORK}.${FILE_SUFFIX}json") + # get diamond address from deployments script (handle both regular and LDA diamonds) + local DEPLOYMENT_FILE="./deployments/${NETWORK}.${FILE_SUFFIX}json" + local DIAMOND_ADDRESS=$(jq -r '.'"$DIAMOND_CONTRACT_NAME" "$DEPLOYMENT_FILE") # if no diamond address was found, throw an error and exit this script if [[ "$DIAMOND_ADDRESS" == "null" ]]; then - error "could not find address for $DIAMOND_CONTRACT_NAME on network $NETWORK in file './deployments/${NETWORK}.${FILE_SUFFIX}json' - exiting script now" + error "could not find address for $DIAMOND_CONTRACT_NAME on network $NETWORK in file '$DEPLOYMENT_FILE' - exiting script now" return 1 fi @@ -100,7 +100,7 @@ function deployFacetAndAddToDiamond() { echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> deploying $FACET_CONTRACT_NAME for $DIAMOND_CONTRACT_NAME now...." # deploy facet - deploySingleContract "$FACET_CONTRACT_NAME" "$NETWORK" "$ENVIRONMENT" "$VERSION" false "$DIAMOND_CONTRACT_NAME" + deploySingleContract "$FACET_CONTRACT_NAME" "$NETWORK" "$ENVIRONMENT" "$VERSION" false # check if function call was successful if [ $? -ne 0 ] @@ -112,12 +112,10 @@ function deployFacetAndAddToDiamond() { # prepare update script name local UPDATE_SCRIPT="Update$FACET_CONTRACT_NAME" - # update diamond - diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$DIAMOND_CONTRACT_NAME" "$UPDATE_SCRIPT" true - - if [ $? -ne 0 ] - then - warning "this call was not successful: diamondUpdateFacet $NETWORK $ENVIRONMENT $DIAMOND_CONTRACT_NAME $UPDATE_SCRIPT true" + diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$DIAMOND_CONTRACT_NAME" "$UPDATE_SCRIPT" + + if [ $? -ne 0 ]; then + warning "this call was not successful: diamondUpdateFacet $NETWORK $ENVIRONMENT $DIAMOND_CONTRACT_NAME $UPDATE_SCRIPT" return 1 fi diff --git a/script/deploy/deploySingleContract.sh b/script/deploy/deploySingleContract.sh index 6da06ad6d..3446ca537 100755 --- a/script/deploy/deploySingleContract.sh +++ b/script/deploy/deploySingleContract.sh @@ -8,6 +8,7 @@ deploySingleContract() { source script/config.sh source script/helperFunctions.sh source script/deploy/resources/contractSpecificReminders.sh + source script/deploy/deployAllLDAContracts.sh # read function arguments into variables local CONTRACT="$1" @@ -15,7 +16,6 @@ deploySingleContract() { local ENVIRONMENT="$3" local VERSION="$4" local EXIT_ON_ERROR="$5" - # load env variables source .env @@ -68,11 +68,38 @@ deploySingleContract() { FILE_EXTENSION=".s.sol" # Handle ZkEVM Chains - # We need to use zksync specific scripts that are able to be compiled for - # the zkvm + # We need to use zksync specific scripts that are able to be compiled for the zkvm. + # Supports 4 combinations: + # 1. Regular + Non-zkEVM = script/deploy/facets/ + # 2. Regular + zkEVM = script/deploy/zksync/ + # 3. LDA + Non-zkEVM = script/deploy/facets/LDA/ + # 4. LDA + zkEVM = script/deploy/zksync/LDA/ + + # Helper function to check if contract is LDA-related + isLDAContract() { + local contract_name="$1" + # Check if contract name is LDA diamond or is in LDA facet + if [[ "$contract_name" == "LiFiDEXAggregatorDiamond" ]] || + [[ -f "script/deploy/facets/LDA/Deploy${contract_name}.s.sol" ]]; then + return 0 # true + else + return 1 # false + fi + } + if isZkEvmNetwork "$NETWORK"; then - DEPLOY_SCRIPT_DIRECTORY="script/deploy/zksync/" + if isLDAContract "$CONTRACT"; then + DEPLOY_SCRIPT_DIRECTORY="script/deploy/zksync/LDA/" + else + DEPLOY_SCRIPT_DIRECTORY="script/deploy/zksync/" + fi FILE_EXTENSION=".zksync.s.sol" + else + if isLDAContract "$CONTRACT"; then + DEPLOY_SCRIPT_DIRECTORY="script/deploy/facets/LDA/" + else + DEPLOY_SCRIPT_DIRECTORY="script/deploy/facets/" + fi fi if [[ -z "$CONTRACT" ]]; then @@ -97,6 +124,30 @@ deploySingleContract() { echo -e "\n\n" fi + # Special case: LiFiDEXAggregatorDiamond is treated as a single external contract + # that triggers the full LDA deployment process + if [[ "$CONTRACT" == "LiFiDEXAggregatorDiamond" ]]; then + echo "[info] LiFiDEXAggregatorDiamond detected - deploying full LDA diamond with all contracts" + echo "[info] Calling deployAllLDAContracts script..." + + # Call the full LDA deployment script + deployAllLDAContracts "$NETWORK" "$ENVIRONMENT" + + # Check if deployment was successful + local LDA_DEPLOYMENT_RESULT=$? + if [[ $LDA_DEPLOYMENT_RESULT -eq 0 ]]; then + echo "[info] LiFiDEXAggregatorDiamond (full LDA deployment) completed successfully" + return 0 + else + error "LiFiDEXAggregatorDiamond (full LDA deployment) failed" + if [[ -z "$EXIT_ON_ERROR" || "$EXIT_ON_ERROR" == "false" ]]; then + return 1 + else + exit 1 + fi + fi + fi + # check if deploy script exists if ! checkIfFileExists "$FULL_SCRIPT_PATH" >/dev/null; then error "could not find deploy script for $CONTRACT in this path: $FULL_SCRIPT_PATH". Aborting deployment. diff --git a/script/deploy/facets/DeployLiFiDEXAggregator.s.sol b/script/deploy/facets/DeployLiFiDEXAggregator.s.sol deleted file mode 100644 index 2b2bd94af..000000000 --- a/script/deploy/facets/DeployLiFiDEXAggregator.s.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.17; - -import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; -import { stdJson } from "forge-std/Script.sol"; -import { LiFiDEXAggregator } from "lifi/Periphery/LiFiDEXAggregator.sol"; - -contract DeployScript is DeployScriptBase { - using stdJson for string; - - constructor() DeployScriptBase("LiFiDEXAggregator") {} - - function run() - public - returns (LiFiDEXAggregator deployed, bytes memory constructorArgs) - { - constructorArgs = getConstructorArgs(); - - deployed = LiFiDEXAggregator( - deploy(type(LiFiDEXAggregator).creationCode) - ); - } - - function getConstructorArgs() internal override returns (bytes memory) { - string memory path = string.concat(root, "/config/global.json"); - string memory json = vm.readFile(path); - - address[] memory priviledgedUsers = new address[](1); - priviledgedUsers[0] = json.readAddress(".pauserWallet"); - - // the original RouteProcessor4.sol is also deployed with address(0) for _bentoBox - - address refundWalletAddress = json.readAddress(".refundWallet"); - - return abi.encode(address(0), priviledgedUsers, refundWalletAddress); - } -} diff --git a/script/deploy/facets/LDA/DeployAlgebraFacet.s.sol b/script/deploy/facets/LDA/DeployAlgebraFacet.s.sol new file mode 100644 index 000000000..96529ff29 --- /dev/null +++ b/script/deploy/facets/LDA/DeployAlgebraFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { AlgebraFacet } from "lifi/Periphery/LDA/Facets/AlgebraFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("AlgebraFacet") {} + + function run() public returns (AlgebraFacet deployed) { + deployed = AlgebraFacet(deploy(type(AlgebraFacet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployCoreRouteFacet.s.sol b/script/deploy/facets/LDA/DeployCoreRouteFacet.s.sol new file mode 100644 index 000000000..52d89c99e --- /dev/null +++ b/script/deploy/facets/LDA/DeployCoreRouteFacet.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { stdJson } from "forge-std/Script.sol"; +import { CoreRouteFacet } from "lifi/Periphery/LDA/Facets/CoreRouteFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("CoreRouteFacet") {} + + function run() public returns (CoreRouteFacet deployed) { + deployed = CoreRouteFacet(deploy(type(CoreRouteFacet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployCurveFacet.s.sol b/script/deploy/facets/LDA/DeployCurveFacet.s.sol new file mode 100644 index 000000000..3d4c674da --- /dev/null +++ b/script/deploy/facets/LDA/DeployCurveFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { CurveFacet } from "lifi/Periphery/LDA/Facets/CurveFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("CurveFacet") {} + + function run() public returns (CurveFacet deployed) { + deployed = CurveFacet(deploy(type(CurveFacet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployIzumiV3Facet.s.sol b/script/deploy/facets/LDA/DeployIzumiV3Facet.s.sol new file mode 100644 index 000000000..79b86480b --- /dev/null +++ b/script/deploy/facets/LDA/DeployIzumiV3Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IzumiV3Facet } from "lifi/Periphery/LDA/Facets/IzumiV3Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("IzumiV3Facet") {} + + function run() public returns (IzumiV3Facet deployed) { + deployed = IzumiV3Facet(deploy(type(IzumiV3Facet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployKatanaV3Facet.s.sol b/script/deploy/facets/LDA/DeployKatanaV3Facet.s.sol new file mode 100644 index 000000000..8ab1003b6 --- /dev/null +++ b/script/deploy/facets/LDA/DeployKatanaV3Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { KatanaV3Facet } from "lifi/Periphery/LDA/Facets/KatanaV3Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("KatanaV3Facet") {} + + function run() public returns (KatanaV3Facet deployed) { + deployed = KatanaV3Facet(deploy(type(KatanaV3Facet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployLiFiDEXAggregatorDiamond.s.sol b/script/deploy/facets/LDA/DeployLiFiDEXAggregatorDiamond.s.sol new file mode 100644 index 000000000..05095d784 --- /dev/null +++ b/script/deploy/facets/LDA/DeployLiFiDEXAggregatorDiamond.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; +import { stdJson } from "forge-std/Script.sol"; +import { LiFiDEXAggregatorDiamond } from "lifi/Periphery/LDA/LiFiDEXAggregatorDiamond.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("LiFiDEXAggregatorDiamond") {} + + function run() + public + returns ( + LiFiDEXAggregatorDiamond deployed, + bytes memory constructorArgs + ) + { + constructorArgs = getConstructorArgs(); + deployed = LiFiDEXAggregatorDiamond( + deploy(type(LiFiDEXAggregatorDiamond).creationCode) + ); + } + + function getConstructorArgs() internal override returns (bytes memory) { + // LDA Diamond uses DiamondCutFacet from regular deployment (shared with regular LiFi Diamond) + string memory path = string.concat( + root, + "/deployments/", + network, + ".", + fileSuffix, + "json" + ); + address diamondCut = _getConfigContractAddress( + path, + ".DiamondCutFacet" + ); + + return abi.encode(deployerAddress, diamondCut); + } +} diff --git a/script/deploy/facets/LDA/DeployNativeWrapperFacet.s.sol b/script/deploy/facets/LDA/DeployNativeWrapperFacet.s.sol new file mode 100644 index 000000000..901c20b27 --- /dev/null +++ b/script/deploy/facets/LDA/DeployNativeWrapperFacet.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { NativeWrapperFacet } from "lifi/Periphery/LDA/Facets/NativeWrapperFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("NativeWrapperFacet") {} + + function run() public returns (NativeWrapperFacet deployed) { + deployed = NativeWrapperFacet( + deploy(type(NativeWrapperFacet).creationCode) + ); + } +} diff --git a/script/deploy/facets/LDA/DeploySyncSwapV2Facet.s.sol b/script/deploy/facets/LDA/DeploySyncSwapV2Facet.s.sol new file mode 100644 index 000000000..45f252e84 --- /dev/null +++ b/script/deploy/facets/LDA/DeploySyncSwapV2Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { SyncSwapV2Facet } from "lifi/Periphery/LDA/Facets/SyncSwapV2Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("SyncSwapV2Facet") {} + + function run() public returns (SyncSwapV2Facet deployed) { + deployed = SyncSwapV2Facet(deploy(type(SyncSwapV2Facet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployUniV2StyleFacet.s.sol b/script/deploy/facets/LDA/DeployUniV2StyleFacet.s.sol new file mode 100644 index 000000000..ccbccfc48 --- /dev/null +++ b/script/deploy/facets/LDA/DeployUniV2StyleFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UniV2StyleFacet } from "lifi/Periphery/LDA/Facets/UniV2StyleFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("UniV2StyleFacet") {} + + function run() public returns (UniV2StyleFacet deployed) { + deployed = UniV2StyleFacet(deploy(type(UniV2StyleFacet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployUniV3StyleFacet.s.sol b/script/deploy/facets/LDA/DeployUniV3StyleFacet.s.sol new file mode 100644 index 000000000..fc825770f --- /dev/null +++ b/script/deploy/facets/LDA/DeployUniV3StyleFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("UniV3StyleFacet") {} + + function run() public returns (UniV3StyleFacet deployed) { + deployed = UniV3StyleFacet(deploy(type(UniV3StyleFacet).creationCode)); + } +} diff --git a/script/deploy/facets/LDA/DeployVelodromeV2Facet.s.sol b/script/deploy/facets/LDA/DeployVelodromeV2Facet.s.sol new file mode 100644 index 000000000..7acd360c6 --- /dev/null +++ b/script/deploy/facets/LDA/DeployVelodromeV2Facet.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { VelodromeV2Facet } from "lifi/Periphery/LDA/Facets/VelodromeV2Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("VelodromeV2Facet") {} + + function run() public returns (VelodromeV2Facet deployed) { + deployed = VelodromeV2Facet( + deploy(type(VelodromeV2Facet).creationCode) + ); + } +} diff --git a/script/deploy/facets/LDA/UpdateAlgebraFacet.s.sol b/script/deploy/facets/LDA/UpdateAlgebraFacet.s.sol new file mode 100644 index 000000000..f87e8393a --- /dev/null +++ b/script/deploy/facets/LDA/UpdateAlgebraFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("AlgebraFacet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateCoreRouteFacet.s.sol b/script/deploy/facets/LDA/UpdateCoreRouteFacet.s.sol new file mode 100644 index 000000000..b0ea64657 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateCoreRouteFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("CoreRouteFacet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateCurveFacet.s.sol b/script/deploy/facets/LDA/UpdateCurveFacet.s.sol new file mode 100644 index 000000000..95c45540a --- /dev/null +++ b/script/deploy/facets/LDA/UpdateCurveFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("CurveFacet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateIzumiV3Facet.s.sol b/script/deploy/facets/LDA/UpdateIzumiV3Facet.s.sol new file mode 100644 index 000000000..e8171b128 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateIzumiV3Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("IzumiV3Facet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateKatanaV3Facet.s.sol b/script/deploy/facets/LDA/UpdateKatanaV3Facet.s.sol new file mode 100644 index 000000000..c6328a577 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateKatanaV3Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("KatanaV3Facet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateLDACoreFacets.s.sol b/script/deploy/facets/LDA/UpdateLDACoreFacets.s.sol new file mode 100644 index 000000000..2fb2959f6 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateLDACoreFacets.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract UpdateLDACoreFacets is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return updateCoreFacets(".ldaCoreFacets"); + } +} diff --git a/script/deploy/facets/LDA/UpdateNativeWrapperFacet.s.sol b/script/deploy/facets/LDA/UpdateNativeWrapperFacet.s.sol new file mode 100644 index 000000000..a20fde375 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateNativeWrapperFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("NativeWrapperFacet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateSyncSwapV2Facet.s.sol b/script/deploy/facets/LDA/UpdateSyncSwapV2Facet.s.sol new file mode 100644 index 000000000..8660a5dec --- /dev/null +++ b/script/deploy/facets/LDA/UpdateSyncSwapV2Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("SyncSwapV2Facet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateUniV2StyleFacet.s.sol b/script/deploy/facets/LDA/UpdateUniV2StyleFacet.s.sol new file mode 100644 index 000000000..8d27a2436 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateUniV2StyleFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("UniV2StyleFacet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateUniV3StyleFacet.s.sol b/script/deploy/facets/LDA/UpdateUniV3StyleFacet.s.sol new file mode 100644 index 000000000..23d245103 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateUniV3StyleFacet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("UniV3StyleFacet"); + } +} diff --git a/script/deploy/facets/LDA/UpdateVelodromeV2Facet.s.sol b/script/deploy/facets/LDA/UpdateVelodromeV2Facet.s.sol new file mode 100644 index 000000000..2f75bf760 --- /dev/null +++ b/script/deploy/facets/LDA/UpdateVelodromeV2Facet.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("VelodromeV2Facet"); + } +} diff --git a/script/deploy/facets/LDA/utils/UpdateLDAScriptBase.sol b/script/deploy/facets/LDA/utils/UpdateLDAScriptBase.sol new file mode 100644 index 000000000..d2680463f --- /dev/null +++ b/script/deploy/facets/LDA/utils/UpdateLDAScriptBase.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { stdJson } from "forge-std/StdJson.sol"; +import { UpdateScriptBase } from "../../utils/UpdateScriptBase.sol"; + +contract UpdateLDAScriptBase is UpdateScriptBase { + using stdJson for string; + + function _getDiamondAddress() internal override returns (address) { + return json.readAddress(".LiFiDEXAggregatorDiamond"); + } + + function _shouldUseDefaultDiamond() internal pure override returns (bool) { + return false; // LDA doesn't use the USE_DEF_DIAMOND env var + } +} diff --git a/script/deploy/facets/UpdateCoreFacets.s.sol b/script/deploy/facets/UpdateCoreFacets.s.sol index f6c8a76cc..68ad9d294 100644 --- a/script/deploy/facets/UpdateCoreFacets.s.sol +++ b/script/deploy/facets/UpdateCoreFacets.s.sol @@ -2,125 +2,12 @@ pragma solidity ^0.8.17; import { UpdateScriptBase } from "./utils/UpdateScriptBase.sol"; -import { stdJson } from "forge-std/StdJson.sol"; -import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; - -contract DeployScript is UpdateScriptBase { - using stdJson for string; - - error FailedToReadCoreFacetsFromConfig(); +contract UpdateCoreFacets is UpdateScriptBase { function run() public returns (address[] memory facets, bytes memory cutData) { - // Read core facets dynamically from global.json config - string memory globalConfigPath = string.concat( - vm.projectRoot(), - "/config/global.json" - ); - string memory globalConfig = vm.readFile(globalConfigPath); - string[] memory coreFacets = globalConfig.readStringArray( - ".coreFacets" - ); - - emit log("Core facets found in config/global.json: "); - emit log_uint(coreFacets.length); - - bytes4[] memory exclude; - - // Check if the loupe was already added to the diamond - bool loupeExists; - try loupe.facetAddresses() returns (address[] memory) { - // If call was successful, loupe exists on diamond already - emit log("Loupe exists on diamond already"); - loupeExists = true; - } catch { - // No need to do anything, just making sure that the flow continues in both cases with try/catch - } - - // Handle DiamondLoupeFacet separately as it needs special treatment - if (!loupeExists) { - emit log("Loupe does not exist on diamond yet"); - address diamondLoupeAddress = _getConfigContractAddress( - path, - ".DiamondLoupeFacet" - ); - bytes4[] memory loupeSelectors = getSelectors( - "DiamondLoupeFacet", - exclude - ); - - buildInitialCut(loupeSelectors, diamondLoupeAddress); - vm.startBroadcast(deployerPrivateKey); - if (cut.length > 0) { - cutter.diamondCut(cut, address(0), ""); - } - vm.stopBroadcast(); - - // Reset diamond cut variable to remove diamondLoupe information - delete cut; - } - - // Process all core facets dynamically - for (uint256 i = 0; i < coreFacets.length; i++) { - string memory facetName = coreFacets[i]; - - // Skip DiamondCutFacet and DiamondLoupeFacet as it was already handled - if ( - keccak256(bytes(facetName)) == - keccak256(bytes("DiamondLoupeFacet")) - ) { - continue; - } - // Skip DiamondLoupeFacet as it was already handled - if ( - keccak256(bytes(facetName)) == - keccak256(bytes("DiamondCutFacet")) - ) { - continue; - } - - emit log("Now adding facet: "); - emit log(facetName); - // Use _getConfigContractAddress which validates the contract exists - address facetAddress = _getConfigContractAddress( - path, - string.concat(".", facetName) - ); - bytes4[] memory selectors = getSelectors(facetName, exclude); - - // at this point we know for sure that diamond loupe exists on diamond - buildDiamondCut(selectors, facetAddress); - // if (loupeExists) { - // buildDiamondCut(selectors, facetAddress); - // } else { - // buildInitialCut(selectors, facetAddress); - // } - } - - // If noBroadcast is activated, we only prepare calldata for sending it to multisig SAFE - if (noBroadcast) { - if (cut.length > 0) { - cutData = abi.encodeWithSelector( - DiamondCutFacet.diamondCut.selector, - cut, - address(0), - "" - ); - } - emit log("=== DIAMOND CUT CALLDATA FOR MANUAL EXECUTION ==="); - emit log_bytes(cutData); - emit log("=== END CALLDATA ==="); - return (facets, cutData); - } - - vm.startBroadcast(deployerPrivateKey); - if (cut.length > 0) { - cutter.diamondCut(cut, address(0), ""); - } - vm.stopBroadcast(); - - facets = loupe.facetAddresses(); + return updateCoreFacets(".coreFacets"); } } diff --git a/script/deploy/facets/utils/DeployScriptBase.sol b/script/deploy/facets/utils/DeployScriptBase.sol index 47191423c..c1c2853be 100644 --- a/script/deploy/facets/utils/DeployScriptBase.sol +++ b/script/deploy/facets/utils/DeployScriptBase.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; import { ScriptBase } from "./ScriptBase.sol"; diff --git a/script/deploy/facets/utils/UpdateScriptBase.sol b/script/deploy/facets/utils/UpdateScriptBase.sol index c23f8a761..9773e5ac5 100644 --- a/script/deploy/facets/utils/UpdateScriptBase.sol +++ b/script/deploy/facets/utils/UpdateScriptBase.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; import { ScriptBase } from "./ScriptBase.sol"; @@ -8,7 +8,7 @@ import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; import { AccessManagerFacet } from "lifi/Facets/AccessManagerFacet.sol"; import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; -contract UpdateScriptBase is ScriptBase { +abstract contract UpdateScriptBase is ScriptBase { using stdJson for string; error InvalidHexDigit(uint8 d); @@ -36,7 +36,7 @@ contract UpdateScriptBase is ScriptBase { bool internal useDefaultDiamond; constructor() { - useDefaultDiamond = vm.envBool("USE_DEF_DIAMOND"); + useDefaultDiamond = _shouldUseDefaultDiamond(); noBroadcast = vm.envOr("NO_BROADCAST", false); path = string.concat( @@ -48,13 +48,164 @@ contract UpdateScriptBase is ScriptBase { "json" ); json = vm.readFile(path); - diamond = useDefaultDiamond - ? json.readAddress(".LiFiDiamond") - : json.readAddress(".LiFiDiamondImmutable"); + diamond = _getDiamondAddress(); cutter = DiamondCutFacet(diamond); loupe = DiamondLoupeFacet(diamond); } + /// @notice Gets the diamond address based on configuration + /// @dev Override this method to customize diamond address selection + function _getDiamondAddress() internal virtual returns (address) { + // Default implementation for regular UpdateScript behavior + return + useDefaultDiamond + ? json.readAddress(".LiFiDiamond") + : json.readAddress(".LiFiDiamondImmutable"); + } + + /// @notice Determines if default diamond should be used + /// @dev Override this method to customize diamond selection logic + function _shouldUseDefaultDiamond() internal virtual returns (bool) { + return vm.envOr("USE_DEF_DIAMOND", true); + } + + /// @notice Updates multiple core facets from global.json configuration + /// @param configKey The key in global.json to read facets from (e.g., ".coreFacets" or ".ldaCoreFacets") + /// @return facets Array of facet addresses after update + /// @return cutData Encoded diamond cut calldata (if noBroadcast is true) + function updateCoreFacets( + string memory configKey + ) + internal + virtual + returns (address[] memory facets, bytes memory cutData) + { + // Read core facets dynamically from global.json config + string memory globalConfigPath = string.concat( + vm.projectRoot(), + "/config/global.json" + ); + string memory globalConfig = vm.readFile(globalConfigPath); + string[] memory coreFacets = globalConfig.readStringArray(configKey); + + emit log_uint(coreFacets.length); + + bytes4[] memory exclude; + + // Check if the loupe was already added to the diamond + bool loupeExists = _checkLoupeExists(); + + // Handle DiamondLoupeFacet separately as it needs special treatment + if (!loupeExists) { + _handleLoupeInstallation(); + } + + // Process all core facets dynamically + for (uint256 i = 0; i < coreFacets.length; i++) { + string memory facetName = coreFacets[i]; + + // Skip DiamondCutFacet and DiamondLoupeFacet as they were already handled + if (_shouldSkipCoreFacet(facetName)) { + continue; + } + + emit log(facetName); + + address facetAddress = _getConfigContractAddress( + path, + string.concat(".", facetName) + ); + + bytes4[] memory selectors = getSelectors(facetName, exclude); + + // at this point we know for sure that diamond loupe exists on diamond + buildDiamondCut(selectors, facetAddress); + } + + // Handle noBroadcast mode and broadcasting + return _finalizeCut(); + } + + /// @notice Checks if DiamondLoupeFacet exists on the diamond + /// @return loupeExists True if loupe exists, false otherwise + function _checkLoupeExists() internal virtual returns (bool loupeExists) { + try loupe.facetAddresses() returns (address[] memory) { + // If call was successful, loupe exists on diamond already + emit log("DiamondLoupeFacet exists on diamond already"); + loupeExists = true; + } catch { + // No need to do anything, just making sure that the flow continues in both cases with try/catch + } + } + + /// @notice Handles DiamondLoupeFacet installation if it doesn't exist + function _handleLoupeInstallation() internal virtual { + emit log("DiamondLoupeFacet does not exist on diamond yet"); + address diamondLoupeAddress = _getConfigContractAddress( + path, + ".DiamondLoupeFacet" + ); + bytes4[] memory loupeSelectors = getSelectors( + "DiamondLoupeFacet", + new bytes4[](0) + ); + + buildInitialCut(loupeSelectors, diamondLoupeAddress); + vm.startBroadcast(deployerPrivateKey); + if (cut.length > 0) { + cutter.diamondCut(cut, address(0), ""); + } + vm.stopBroadcast(); + + // Reset diamond cut variable to remove diamondLoupe information + delete cut; + } + + /// @notice Determines if a facet should be skipped during core facets update + /// @param facetName The name of the facet to check + /// @return True if facet should be skipped, false otherwise + function _shouldSkipCoreFacet( + string memory facetName + ) internal pure virtual returns (bool) { + return (keccak256(bytes(facetName)) == + keccak256(bytes("DiamondLoupeFacet")) || + keccak256(bytes(facetName)) == + keccak256(bytes("DiamondCutFacet"))); + } + + /// @notice Finalizes the diamond cut operation + /// @return facets Array of facet addresses after update + /// @return cutData Encoded diamond cut calldata (if noBroadcast is true) + function _finalizeCut() + internal + virtual + returns (address[] memory facets, bytes memory cutData) + { + // If noBroadcast is activated, we only prepare calldata for sending it to multisig SAFE + if (noBroadcast) { + if (cut.length > 0) { + cutData = abi.encodeWithSelector( + DiamondCutFacet.diamondCut.selector, + cut, + address(0), + "" + ); + } + emit log("=== DIAMOND CUT CALLDATA FOR MANUAL EXECUTION ==="); + emit log_bytes(cutData); + emit log("=== END CALLDATA ==="); + return (facets, cutData); + } + + vm.startBroadcast(deployerPrivateKey); + if (cut.length > 0) { + cutter.diamondCut(cut, address(0), ""); + } + vm.stopBroadcast(); + + facets = loupe.facetAddresses(); + } + function update( string memory name ) @@ -62,7 +213,11 @@ contract UpdateScriptBase is ScriptBase { virtual returns (address[] memory facets, bytes memory cutData) { - address facet = json.readAddress(string.concat(".", name)); + address facet = _getConfigContractAddress( + path, + string.concat(".", name) + ); + bytes4[] memory excludes = getExcludes(); bytes memory callData = getCallData(); diff --git a/script/deploy/ldaHealthCheck.ts b/script/deploy/ldaHealthCheck.ts new file mode 100755 index 000000000..2de596cc2 --- /dev/null +++ b/script/deploy/ldaHealthCheck.ts @@ -0,0 +1,379 @@ +#!/usr/bin/env node + +// LiFi DEX Aggregator (LDA) diamond health check script +// - validates LDA diamond contract deployment +// - checks availability of LDA core facets (shared with regular LiFi Diamond) +// - verifies all facets are properly registered in the diamond +// - validates diamond ownership (multisig for production, deployer for staging) + +import { execSync } from 'child_process' + +import { defineCommand, runMain } from 'citty' +import { consola } from 'consola' + +import type { SupportedChain } from '../common/types' +import { getRpcUrl } from '../demoScripts/utils/demoScriptHelpers' + +const errors: string[] = [] + +// Helper function to check if contract is deployed using cast +const checkIsDeployedWithCast = async ( + contractName: string, + deployedContracts: Record, + rpcUrl: string +): Promise => { + if (!deployedContracts[contractName]) return false + + try { + const address = deployedContracts[contractName] + const result = execSync(`cast code ${address} --rpc-url "${rpcUrl}"`, { + encoding: 'utf8', + stdio: 'pipe', + }).trim() + + // If the result is '0x' or empty, contract is not deployed + return result !== '0x' && result !== '' + } catch (error) { + return false + } +} + +const main = defineCommand({ + meta: { + name: 'LDA Diamond Health Check', + description: 'Check that the LDA diamond is configured correctly', + }, + args: { + network: { + type: 'string', + description: 'EVM network to check', + required: true, + }, + environment: { + type: 'string', + description: 'Environment to check (staging or production)', + default: 'production', + }, + }, + async run({ args }) { + const { network, environment } = args + + // Skip tronshasta testnet but allow tron mainnet + if (network.toLowerCase() === 'tronshasta') { + consola.info('Health checks are not implemented for Tron Shasta testnet.') + consola.info('Skipping all tests.') + process.exit(0) + } + + // Determine if we're working with Tron mainnet + const isTron = network.toLowerCase() === 'tron' + + if (isTron) { + consola.info('LDA health checks for Tron are not yet implemented.') + consola.info('Skipping all tests.') + process.exit(0) + } + + // Validate environment + if (environment !== 'staging' && environment !== 'production') { + consola.error( + `Invalid environment: ${environment}. Must be 'staging' or 'production'.` + ) + process.exit(1) + } + + // Load deployment file (contains LiFiDEXAggregatorDiamond address and shared infrastructure) + let deployedContracts: Record = {} + const productionDeploymentFile = `../../deployments/${network.toLowerCase()}.json` + + if (environment === 'staging') { + const stagingFile = `../../deployments/${network.toLowerCase()}.staging.json` + consola.info(`Loading staging deployment file: ${stagingFile}`) + try { + const { default: contracts } = await import(stagingFile, { + with: { type: 'json' }, + }) + deployedContracts = contracts + consola.info( + `Successfully loaded ${ + Object.keys(contracts).length + } contracts from staging file` + ) + } catch (error) { + consola.error(`Failed to load staging deployment file: ${stagingFile}`) + consola.error(`Error: ${error}`) + consola.error( + 'Cannot proceed with staging environment without staging deployment file.' + ) + process.exit(1) + } + } + // Production - use production deployment file + else { + consola.info( + `Loading production deployment file: ${productionDeploymentFile}` + ) + try { + const { default: contracts } = await import(productionDeploymentFile, { + with: { type: 'json' }, + }) + deployedContracts = contracts + consola.info( + `Successfully loaded ${ + Object.keys(contracts).length + } contracts from production file` + ) + } catch (error) { + consola.error( + `Failed to load production deployment file: ${productionDeploymentFile}` + ) + consola.error(`Error: ${error}`) + consola.error('Cannot verify LDA diamond and core facets availability.') + process.exit(1) + } + } + + // Load LDA-specific facet information + const ldaDeploymentFile = + environment === 'production' + ? `../../deployments/${network.toLowerCase()}.diamond.lda.json` + : `../../deployments/${network.toLowerCase()}.diamond.lda.${environment}.json` + let ldaFacetInfo: Record< + string, + { Facets?: Record } + > = {} + + try { + const { default: ldaData } = await import(ldaDeploymentFile, { + with: { type: 'json' }, + }) + ldaFacetInfo = ldaData + } catch (error) { + consola.error(`Failed to load LDA facet file: ${ldaDeploymentFile}`) + consola.error( + `Please ensure LDA diamond logs are updated after deployment.` + ) + process.exit(1) + } + + // Load global config for LDA core facets + const globalConfig = await import('../../config/global.json', { + with: { type: 'json' }, + }) + const networksConfigModule = await import('../../config/networks.json', { + with: { type: 'json' }, + }) + const networksConfig = networksConfigModule.default + + // Get LDA core facets from global config + const ldaCoreFacets = globalConfig.ldaCoreFacets || [] + + // Get RPC URL + const rpcUrl = getRpcUrl(network as SupportedChain) + if (!rpcUrl) { + consola.error(`No RPC URL found for network: ${network}`) + process.exit(1) + } + + consola.info( + `Running LDA Diamond post deployment checks for ${environment}...\n` + ) + + // ╭─────────────────────────────────────────────────────────╮ + // │ Check LDA diamond contract full deployment │ + // ╰─────────────────────────────────────────────────────────╯ + consola.box('Checking LDA diamond contract full deployment...') + + const diamondAddress = deployedContracts['LiFiDEXAggregatorDiamond'] + consola.info( + `Looking for LiFiDEXAggregatorDiamond at address: ${diamondAddress}` + ) + consola.info( + `Available contracts in deployedContracts: ${Object.keys( + deployedContracts + ) + .filter((k) => k.includes('LiFi')) + .join(', ')}` + ) + + const diamondDeployed = await checkIsDeployedWithCast( + 'LiFiDEXAggregatorDiamond', + deployedContracts, + rpcUrl + ) + + if (!diamondDeployed) { + logError('LiFiDEXAggregatorDiamond not deployed') + finish() + } else consola.success('LiFiDEXAggregatorDiamond deployed') + + // ╭─────────────────────────────────────────────────────────╮ + // │ Check LDA core facets availability │ + // ╰─────────────────────────────────────────────────────────╯ + consola.box('Checking LDA Core Facets Availability...') + consola.info( + 'LDA core facets are shared with regular LiFi Diamond and stored in regular deployment file' + ) + + for (const facet of ldaCoreFacets) { + // Check if core facet is deployed in regular deployment file (shared with LiFi Diamond) + const isDeployed = await checkIsDeployedWithCast( + facet, + deployedContracts, + rpcUrl + ) + + if (!isDeployed) { + logError( + `LDA Core Facet ${facet} not found in regular deployment file - please deploy regular LiFi Diamond first` + ) + continue + } + consola.success(`LDA Core Facet ${facet} available in regular deployment`) + } + + // ╭─────────────────────────────────────────────────────────╮ + // │ Check that LDA facets are registered │ + // ╰─────────────────────────────────────────────────────────╯ + consola.box('Checking LDA facets registered in diamond...') + + let registeredFacets: string[] = [] + try { + // Use cast to call facets() function + const rawString = execSync( + `cast call "${diamondAddress}" "facets() returns ((address,bytes4[])[])" --rpc-url "${rpcUrl}"`, + { encoding: 'utf8', stdio: 'pipe' } + ) + + const jsonCompatibleString = rawString + .replace(/\(/g, '[') + .replace(/\)/g, ']') + .replace(/0x[0-9a-fA-F]+/g, '"$&"') + + const onChainFacets = JSON.parse(jsonCompatibleString) + + if (Array.isArray(onChainFacets)) { + // Create mapping from addresses to facet names + // For core facets, use addresses from production deployment file + // For non-core facets, use addresses from LDA deployment file + const configFacetsByAddress: Record = {} + + // Add core facets from production deployment file + for (const facet of ldaCoreFacets) { + const address = deployedContracts[facet] + if (address) configFacetsByAddress[address.toLowerCase()] = facet + } + + // Add non-core facets from LDA facet info file + const diamondFacets = + ldaFacetInfo['LiFiDEXAggregatorDiamond']?.Facets || {} + Object.entries(diamondFacets).forEach(([address, facetData]) => { + const facetName = facetData.Name + if (facetName && !ldaCoreFacets.includes(facetName)) + configFacetsByAddress[address.toLowerCase()] = facetName + }) + + registeredFacets = onChainFacets + .map(([address]) => configFacetsByAddress[address.toLowerCase()]) + .filter(Boolean) as string[] + } + } catch (error) { + consola.warn( + 'Unable to call facets() - skipping facet registration check' + ) + consola.warn('Error:', (error as Error).message) + } + + // Check core facets registration + for (const facet of ldaCoreFacets) + if (!registeredFacets.includes(facet)) + logError(`LDA Core Facet ${facet} not registered in Diamond`) + else consola.success(`LDA Core Facet ${facet} registered in Diamond`) + + // Check non-core facets registration + const diamondFacets = ldaFacetInfo['LiFiDEXAggregatorDiamond']?.Facets || {} + const nonCoreFacets = Object.values(diamondFacets) + .map((facetData) => facetData.Name) + .filter( + (name): name is string => + name !== undefined && !ldaCoreFacets.includes(name) + ) + + for (const facet of nonCoreFacets) + if (!registeredFacets.includes(facet)) + logError(`LDA Non-Core Facet ${facet} not registered in Diamond`) + else consola.success(`LDA Non-Core Facet ${facet} registered in Diamond`) + + // ╭─────────────────────────────────────────────────────────╮ + // │ Check LDA Diamond ownership │ + // ╰─────────────────────────────────────────────────────────╯ + + if (environment === 'production') { + consola.box('Checking LDA Diamond ownership...') + + try { + const owner = execSync( + `cast call "${diamondAddress}" "owner() returns (address)" --rpc-url "${rpcUrl}"`, + { encoding: 'utf8', stdio: 'pipe' } + ).trim() + + consola.info(`LiFiDEXAggregatorDiamond current owner: ${owner}`) + + // Get expected multisig address from networks.json + const expectedMultisigAddress = + networksConfig[network.toLowerCase() as SupportedChain]?.safeAddress + + if (!expectedMultisigAddress) { + logError( + `No multisig address (safeAddress) found in networks.json for network ${network}` + ) + } else { + consola.info( + `Expected multisig address from networks.json: ${expectedMultisigAddress}` + ) + + if (owner.toLowerCase() === expectedMultisigAddress.toLowerCase()) { + consola.success( + `✅ LiFiDEXAggregatorDiamond is correctly owned by multisig: ${expectedMultisigAddress}` + ) + } else { + logError( + `❌ LiFiDEXAggregatorDiamond ownership mismatch! Current owner: ${owner}, Expected multisig: ${expectedMultisigAddress}` + ) + } + } + } catch (error) { + logError( + `Failed to check LiFiDEXAggregatorDiamond ownership: ${ + (error as Error).message + }` + ) + } + } else { + consola.info( + '⏭️ Skipping LDA Diamond ownership check for staging environment' + ) + } + + finish() + }, +}) + +const logError = (msg: string) => { + consola.error(msg) + errors.push(msg) +} + +const finish = () => { + // this line ensures that all logs are actually written before the script ends + process.stdout.write('', () => process.stdout.end()) + if (errors.length) { + consola.error(`${errors.length} Errors found in LDA Diamond deployment`) + process.exit(1) + } else { + consola.success('LDA Diamond deployment checks passed') + process.exit(0) + } +} + +runMain(main) diff --git a/script/deploy/zksync/DeployLiFiDEXAggregator.zksync.s.sol b/script/deploy/zksync/DeployLiFiDEXAggregator.zksync.s.sol deleted file mode 100644 index c579ad0ef..000000000 --- a/script/deploy/zksync/DeployLiFiDEXAggregator.zksync.s.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.17; - -import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; -import { stdJson } from "forge-std/Script.sol"; -import { LiFiDEXAggregator } from "lifi/Periphery/LiFiDEXAggregator.sol"; - -contract DeployScript is DeployScriptBase { - using stdJson for string; - - constructor() DeployScriptBase("LiFiDEXAggregator") {} - - function run() - public - returns (LiFiDEXAggregator deployed, bytes memory constructorArgs) - { - constructorArgs = getConstructorArgs(); - - deployed = LiFiDEXAggregator( - deploy(type(LiFiDEXAggregator).creationCode) - ); - } - - function getConstructorArgs() internal override returns (bytes memory) { - string memory path = string.concat(root, "/config/global.json"); - string memory json = vm.readFile(path); - - address[] memory priviledgedUsers = new address[](1); - priviledgedUsers[0] = json.readAddress(".pauserWallet"); - - // the original RouteProcessor4.sol is also deployed with address(0) for _bentoBox - address refundWalletAddress = json.readAddress(".refundWallet"); - - return abi.encode(address(0), priviledgedUsers, refundWalletAddress); - } -} diff --git a/script/deploy/zksync/LDA/DeployAlgebraFacet.zksync.s.sol b/script/deploy/zksync/LDA/DeployAlgebraFacet.zksync.s.sol new file mode 100644 index 000000000..96529ff29 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployAlgebraFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { AlgebraFacet } from "lifi/Periphery/LDA/Facets/AlgebraFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("AlgebraFacet") {} + + function run() public returns (AlgebraFacet deployed) { + deployed = AlgebraFacet(deploy(type(AlgebraFacet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployCoreRouteFacet.zksync.s.sol b/script/deploy/zksync/LDA/DeployCoreRouteFacet.zksync.s.sol new file mode 100644 index 000000000..52d89c99e --- /dev/null +++ b/script/deploy/zksync/LDA/DeployCoreRouteFacet.zksync.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { stdJson } from "forge-std/Script.sol"; +import { CoreRouteFacet } from "lifi/Periphery/LDA/Facets/CoreRouteFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("CoreRouteFacet") {} + + function run() public returns (CoreRouteFacet deployed) { + deployed = CoreRouteFacet(deploy(type(CoreRouteFacet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployCurveFacet.zksync.s.sol b/script/deploy/zksync/LDA/DeployCurveFacet.zksync.s.sol new file mode 100644 index 000000000..3d4c674da --- /dev/null +++ b/script/deploy/zksync/LDA/DeployCurveFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { CurveFacet } from "lifi/Periphery/LDA/Facets/CurveFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("CurveFacet") {} + + function run() public returns (CurveFacet deployed) { + deployed = CurveFacet(deploy(type(CurveFacet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployIzumiV3Facet.zksync.s.sol b/script/deploy/zksync/LDA/DeployIzumiV3Facet.zksync.s.sol new file mode 100644 index 000000000..79b86480b --- /dev/null +++ b/script/deploy/zksync/LDA/DeployIzumiV3Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IzumiV3Facet } from "lifi/Periphery/LDA/Facets/IzumiV3Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("IzumiV3Facet") {} + + function run() public returns (IzumiV3Facet deployed) { + deployed = IzumiV3Facet(deploy(type(IzumiV3Facet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployKatanaV3Facet.zksync.s.sol b/script/deploy/zksync/LDA/DeployKatanaV3Facet.zksync.s.sol new file mode 100644 index 000000000..8ab1003b6 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployKatanaV3Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { KatanaV3Facet } from "lifi/Periphery/LDA/Facets/KatanaV3Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("KatanaV3Facet") {} + + function run() public returns (KatanaV3Facet deployed) { + deployed = KatanaV3Facet(deploy(type(KatanaV3Facet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployLiFiDEXAggregatorDiamond.zksync.s.sol b/script/deploy/zksync/LDA/DeployLiFiDEXAggregatorDiamond.zksync.s.sol new file mode 100644 index 000000000..cd992b664 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployLiFiDEXAggregatorDiamond.zksync.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { stdJson } from "forge-std/Script.sol"; +import { LiFiDEXAggregatorDiamond } from "lifi/Periphery/LDA/LiFiDEXAggregatorDiamond.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + using stdJson for string; + + constructor() DeployScriptBase("LiFiDEXAggregatorDiamond") {} + + function run() + public + returns ( + LiFiDEXAggregatorDiamond deployed, + bytes memory constructorArgs + ) + { + constructorArgs = getConstructorArgs(); + deployed = LiFiDEXAggregatorDiamond( + deploy(type(LiFiDEXAggregatorDiamond).creationCode) + ); + } + + function getConstructorArgs() internal override returns (bytes memory) { + // LDA Diamond uses DiamondCutFacet from regular deployment (shared with regular LiFi Diamond) + string memory path = string.concat( + root, + "/deployments/", + network, + ".", + fileSuffix, + "json" + ); + address diamondCut = _getConfigContractAddress( + path, + ".DiamondCutFacet" + ); + + return abi.encode(deployerAddress, diamondCut); + } +} diff --git a/script/deploy/zksync/LDA/DeployNativeWrapperFacet.zksync.s.sol b/script/deploy/zksync/LDA/DeployNativeWrapperFacet.zksync.s.sol new file mode 100644 index 000000000..901c20b27 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployNativeWrapperFacet.zksync.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { NativeWrapperFacet } from "lifi/Periphery/LDA/Facets/NativeWrapperFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("NativeWrapperFacet") {} + + function run() public returns (NativeWrapperFacet deployed) { + deployed = NativeWrapperFacet( + deploy(type(NativeWrapperFacet).creationCode) + ); + } +} diff --git a/script/deploy/zksync/LDA/DeploySyncSwapV2Facet.zksync.s.sol b/script/deploy/zksync/LDA/DeploySyncSwapV2Facet.zksync.s.sol new file mode 100644 index 000000000..45f252e84 --- /dev/null +++ b/script/deploy/zksync/LDA/DeploySyncSwapV2Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { SyncSwapV2Facet } from "lifi/Periphery/LDA/Facets/SyncSwapV2Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("SyncSwapV2Facet") {} + + function run() public returns (SyncSwapV2Facet deployed) { + deployed = SyncSwapV2Facet(deploy(type(SyncSwapV2Facet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployUniV2StyleFacet.zksync.s.sol b/script/deploy/zksync/LDA/DeployUniV2StyleFacet.zksync.s.sol new file mode 100644 index 000000000..ccbccfc48 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployUniV2StyleFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UniV2StyleFacet } from "lifi/Periphery/LDA/Facets/UniV2StyleFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("UniV2StyleFacet") {} + + function run() public returns (UniV2StyleFacet deployed) { + deployed = UniV2StyleFacet(deploy(type(UniV2StyleFacet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployUniV3StyleFacet.zksync.s.sol b/script/deploy/zksync/LDA/DeployUniV3StyleFacet.zksync.s.sol new file mode 100644 index 000000000..ab168ece9 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployUniV3StyleFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("UniV3StyleFacet") {} + + function run() public returns (UniV3StyleFacet deployed) { + deployed = UniV3StyleFacet(deploy(type(UniV3StyleFacet).creationCode)); + } +} diff --git a/script/deploy/zksync/LDA/DeployVelodromeV2Facet.zksync.s.sol b/script/deploy/zksync/LDA/DeployVelodromeV2Facet.zksync.s.sol new file mode 100644 index 000000000..7acd360c6 --- /dev/null +++ b/script/deploy/zksync/LDA/DeployVelodromeV2Facet.zksync.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { VelodromeV2Facet } from "lifi/Periphery/LDA/Facets/VelodromeV2Facet.sol"; +import { DeployScriptBase } from "../utils/DeployScriptBase.sol"; + +contract DeployScript is DeployScriptBase { + constructor() DeployScriptBase("VelodromeV2Facet") {} + + function run() public returns (VelodromeV2Facet deployed) { + deployed = VelodromeV2Facet( + deploy(type(VelodromeV2Facet).creationCode) + ); + } +} diff --git a/script/deploy/zksync/LDA/UpdateAlgebraFacet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateAlgebraFacet.zksync.s.sol new file mode 100644 index 000000000..f87e8393a --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateAlgebraFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("AlgebraFacet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateCoreRouteFacet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateCoreRouteFacet.zksync.s.sol new file mode 100644 index 000000000..b0ea64657 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateCoreRouteFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("CoreRouteFacet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateCurveFacet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateCurveFacet.zksync.s.sol new file mode 100644 index 000000000..95c45540a --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateCurveFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("CurveFacet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateIzumiV3Facet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateIzumiV3Facet.zksync.s.sol new file mode 100644 index 000000000..e8171b128 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateIzumiV3Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("IzumiV3Facet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateKatanaV3Facet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateKatanaV3Facet.zksync.s.sol new file mode 100644 index 000000000..c6328a577 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateKatanaV3Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("KatanaV3Facet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateLDACoreFacets.zksync.s.sol b/script/deploy/zksync/LDA/UpdateLDACoreFacets.zksync.s.sol new file mode 100644 index 000000000..2fb2959f6 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateLDACoreFacets.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract UpdateLDACoreFacets is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return updateCoreFacets(".ldaCoreFacets"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateNativeWrapperFacet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateNativeWrapperFacet.zksync.s.sol new file mode 100644 index 000000000..a20fde375 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateNativeWrapperFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("NativeWrapperFacet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateSyncSwapV2Facet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateSyncSwapV2Facet.zksync.s.sol new file mode 100644 index 000000000..8660a5dec --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateSyncSwapV2Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("SyncSwapV2Facet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateUniV2StyleFacet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateUniV2StyleFacet.zksync.s.sol new file mode 100644 index 000000000..8d27a2436 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateUniV2StyleFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("UniV2StyleFacet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateUniV3StyleFacet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateUniV3StyleFacet.zksync.s.sol new file mode 100644 index 000000000..23d245103 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateUniV3StyleFacet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("UniV3StyleFacet"); + } +} diff --git a/script/deploy/zksync/LDA/UpdateVelodromeV2Facet.zksync.s.sol b/script/deploy/zksync/LDA/UpdateVelodromeV2Facet.zksync.s.sol new file mode 100644 index 000000000..2f75bf760 --- /dev/null +++ b/script/deploy/zksync/LDA/UpdateVelodromeV2Facet.zksync.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UpdateLDAScriptBase } from "./utils/UpdateLDAScriptBase.sol"; + +contract DeployScript is UpdateLDAScriptBase { + function run() + public + returns (address[] memory facets, bytes memory cutData) + { + return update("VelodromeV2Facet"); + } +} diff --git a/script/deploy/zksync/LDA/utils/UpdateLDAScriptBase.sol b/script/deploy/zksync/LDA/utils/UpdateLDAScriptBase.sol new file mode 100644 index 000000000..762fc4285 --- /dev/null +++ b/script/deploy/zksync/LDA/utils/UpdateLDAScriptBase.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { stdJson } from "forge-std/StdJson.sol"; +import { UpdateScriptBase } from "../../utils/UpdateScriptBase.sol"; + +contract UpdateLDAScriptBase is UpdateScriptBase { + using stdJson for string; + + function _getDiamondAddress() internal override returns (address) { + return json.readAddress(".LiFiDEXAggregatorDiamond"); + } +} diff --git a/script/deploy/zksync/UpdateCoreFacets.zksync.s.sol b/script/deploy/zksync/UpdateCoreFacets.zksync.s.sol index f6c8a76cc..68ad9d294 100644 --- a/script/deploy/zksync/UpdateCoreFacets.zksync.s.sol +++ b/script/deploy/zksync/UpdateCoreFacets.zksync.s.sol @@ -2,125 +2,12 @@ pragma solidity ^0.8.17; import { UpdateScriptBase } from "./utils/UpdateScriptBase.sol"; -import { stdJson } from "forge-std/StdJson.sol"; -import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; - -contract DeployScript is UpdateScriptBase { - using stdJson for string; - - error FailedToReadCoreFacetsFromConfig(); +contract UpdateCoreFacets is UpdateScriptBase { function run() public returns (address[] memory facets, bytes memory cutData) { - // Read core facets dynamically from global.json config - string memory globalConfigPath = string.concat( - vm.projectRoot(), - "/config/global.json" - ); - string memory globalConfig = vm.readFile(globalConfigPath); - string[] memory coreFacets = globalConfig.readStringArray( - ".coreFacets" - ); - - emit log("Core facets found in config/global.json: "); - emit log_uint(coreFacets.length); - - bytes4[] memory exclude; - - // Check if the loupe was already added to the diamond - bool loupeExists; - try loupe.facetAddresses() returns (address[] memory) { - // If call was successful, loupe exists on diamond already - emit log("Loupe exists on diamond already"); - loupeExists = true; - } catch { - // No need to do anything, just making sure that the flow continues in both cases with try/catch - } - - // Handle DiamondLoupeFacet separately as it needs special treatment - if (!loupeExists) { - emit log("Loupe does not exist on diamond yet"); - address diamondLoupeAddress = _getConfigContractAddress( - path, - ".DiamondLoupeFacet" - ); - bytes4[] memory loupeSelectors = getSelectors( - "DiamondLoupeFacet", - exclude - ); - - buildInitialCut(loupeSelectors, diamondLoupeAddress); - vm.startBroadcast(deployerPrivateKey); - if (cut.length > 0) { - cutter.diamondCut(cut, address(0), ""); - } - vm.stopBroadcast(); - - // Reset diamond cut variable to remove diamondLoupe information - delete cut; - } - - // Process all core facets dynamically - for (uint256 i = 0; i < coreFacets.length; i++) { - string memory facetName = coreFacets[i]; - - // Skip DiamondCutFacet and DiamondLoupeFacet as it was already handled - if ( - keccak256(bytes(facetName)) == - keccak256(bytes("DiamondLoupeFacet")) - ) { - continue; - } - // Skip DiamondLoupeFacet as it was already handled - if ( - keccak256(bytes(facetName)) == - keccak256(bytes("DiamondCutFacet")) - ) { - continue; - } - - emit log("Now adding facet: "); - emit log(facetName); - // Use _getConfigContractAddress which validates the contract exists - address facetAddress = _getConfigContractAddress( - path, - string.concat(".", facetName) - ); - bytes4[] memory selectors = getSelectors(facetName, exclude); - - // at this point we know for sure that diamond loupe exists on diamond - buildDiamondCut(selectors, facetAddress); - // if (loupeExists) { - // buildDiamondCut(selectors, facetAddress); - // } else { - // buildInitialCut(selectors, facetAddress); - // } - } - - // If noBroadcast is activated, we only prepare calldata for sending it to multisig SAFE - if (noBroadcast) { - if (cut.length > 0) { - cutData = abi.encodeWithSelector( - DiamondCutFacet.diamondCut.selector, - cut, - address(0), - "" - ); - } - emit log("=== DIAMOND CUT CALLDATA FOR MANUAL EXECUTION ==="); - emit log_bytes(cutData); - emit log("=== END CALLDATA ==="); - return (facets, cutData); - } - - vm.startBroadcast(deployerPrivateKey); - if (cut.length > 0) { - cutter.diamondCut(cut, address(0), ""); - } - vm.stopBroadcast(); - - facets = loupe.facetAddresses(); + return updateCoreFacets(".coreFacets"); } } diff --git a/script/deploy/zksync/utils/DeployScriptBase.sol b/script/deploy/zksync/utils/DeployScriptBase.sol index 025a255bb..415595ee1 100644 --- a/script/deploy/zksync/utils/DeployScriptBase.sol +++ b/script/deploy/zksync/utils/DeployScriptBase.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; -import { ScriptBase } from "../../facets/utils/ScriptBase.sol"; -import { LibAsset } from "lifi/Libraries/LibAsset.sol"; import { stdJson } from "forge-std/Script.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { ScriptBase } from "./ScriptBase.sol"; interface IContractDeployer { function getNewAddressCreate2( diff --git a/script/deploy/zksync/utils/UpdateScriptBase.sol b/script/deploy/zksync/utils/UpdateScriptBase.sol index cb158df4a..202ae2705 100644 --- a/script/deploy/zksync/utils/UpdateScriptBase.sol +++ b/script/deploy/zksync/utils/UpdateScriptBase.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; import { ScriptBase } from "./ScriptBase.sol"; @@ -7,7 +7,7 @@ import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; -contract UpdateScriptBase is ScriptBase { +abstract contract UpdateScriptBase is ScriptBase { using stdJson for string; struct FunctionSignature { @@ -42,13 +42,21 @@ contract UpdateScriptBase is ScriptBase { "json" ); json = vm.readFile(path); - diamond = useDefaultDiamond - ? json.readAddress(".LiFiDiamond") - : json.readAddress(".LiFiDiamondImmutable"); + diamond = _getDiamondAddress(); cutter = DiamondCutFacet(diamond); loupe = DiamondLoupeFacet(diamond); } + /// @notice Gets the diamond address based on configuration + /// @dev Override this method to customize diamond address selection + function _getDiamondAddress() internal virtual returns (address) { + // Default implementation for regular UpdateScript behavior + return + useDefaultDiamond + ? json.readAddress(".LiFiDiamond") + : json.readAddress(".LiFiDiamondImmutable"); + } + function update( string memory name ) @@ -56,7 +64,10 @@ contract UpdateScriptBase is ScriptBase { virtual returns (address[] memory facets, bytes memory cutData) { - address facet = json.readAddress(string.concat(".", name)); + address facet = _getConfigContractAddress( + path, + string.concat(".", name) + ); bytes4[] memory excludes = getExcludes(); bytes memory callData = getCallData(); @@ -90,6 +101,143 @@ contract UpdateScriptBase is ScriptBase { vm.stopBroadcast(); } + /// @notice Updates multiple core facets from global.json configuration + /// @param configKey The key in global.json to read facets from (e.g., ".coreFacets" or ".ldaCoreFacets") + /// @return facets Array of facet addresses after update + /// @return cutData Encoded diamond cut calldata (if noBroadcast is true) + function updateCoreFacets( + string memory configKey + ) + internal + virtual + returns (address[] memory facets, bytes memory cutData) + { + // Read core facets dynamically from global.json config + string memory globalConfigPath = string.concat( + vm.projectRoot(), + "/config/global.json" + ); + string memory globalConfig = vm.readFile(globalConfigPath); + string[] memory coreFacets = globalConfig.readStringArray(configKey); + + emit log_uint(coreFacets.length); + + bytes4[] memory exclude; + + // Check if the loupe was already added to the diamond + bool loupeExists = _checkLoupeExists(); + + // Handle DiamondLoupeFacet separately as it needs special treatment + if (!loupeExists) { + _handleLoupeInstallation(); + } + + // Process all core facets dynamically + for (uint256 i = 0; i < coreFacets.length; i++) { + string memory facetName = coreFacets[i]; + + // Skip DiamondCutFacet and DiamondLoupeFacet as they were already handled + if (_shouldSkipCoreFacet(facetName)) { + continue; + } + + emit log(facetName); + + address facetAddress = _getConfigContractAddress( + path, + string.concat(".", facetName) + ); + + bytes4[] memory selectors = getSelectors(facetName, exclude); + + // at this point we know for sure that diamond loupe exists on diamond + buildDiamondCut(selectors, facetAddress); + } + + // Handle noBroadcast mode and broadcasting + return _finalizeCut(); + } + + /// @notice Checks if DiamondLoupeFacet exists on the diamond + /// @return loupeExists True if loupe exists, false otherwise + function _checkLoupeExists() internal virtual returns (bool loupeExists) { + try loupe.facetAddresses() returns (address[] memory) { + // If call was successful, loupe exists on diamond already + emit log("DiamondLoupeFacet exists on diamond already"); + loupeExists = true; + } catch { + // No need to do anything, just making sure that the flow continues in both cases with try/catch + } + } + + /// @notice Handles DiamondLoupeFacet installation if it doesn't exist + function _handleLoupeInstallation() internal virtual { + emit log("DiamondLoupeFacet does not exist on diamond yet"); + address diamondLoupeAddress = _getConfigContractAddress( + path, + ".DiamondLoupeFacet" + ); + bytes4[] memory loupeSelectors = getSelectors( + "DiamondLoupeFacet", + new bytes4[](0) + ); + + buildInitialCut(loupeSelectors, diamondLoupeAddress); + vm.startBroadcast(deployerPrivateKey); + if (cut.length > 0) { + cutter.diamondCut(cut, address(0), ""); + } + vm.stopBroadcast(); + + // Reset diamond cut variable to remove diamondLoupe information + delete cut; + } + + /// @notice Determines if a facet should be skipped during core facets update + /// @param facetName The name of the facet to check + /// @return True if facet should be skipped, false otherwise + function _shouldSkipCoreFacet( + string memory facetName + ) internal pure virtual returns (bool) { + return (keccak256(bytes(facetName)) == + keccak256(bytes("DiamondLoupeFacet")) || + keccak256(bytes(facetName)) == + keccak256(bytes("DiamondCutFacet"))); + } + + /// @notice Finalizes the diamond cut operation + /// @return facets Array of facet addresses after update + /// @return cutData Encoded diamond cut calldata (if noBroadcast is true) + function _finalizeCut() + internal + virtual + returns (address[] memory facets, bytes memory cutData) + { + // If noBroadcast is activated, we only prepare calldata for sending it to multisig SAFE + if (noBroadcast) { + if (cut.length > 0) { + cutData = abi.encodeWithSelector( + DiamondCutFacet.diamondCut.selector, + cut, + address(0), + "" + ); + } + emit log("=== DIAMOND CUT CALLDATA FOR MANUAL EXECUTION ==="); + emit log_bytes(cutData); + emit log("=== END CALLDATA ==="); + return (facets, cutData); + } + + vm.startBroadcast(deployerPrivateKey); + if (cut.length > 0) { + cutter.diamondCut(cut, address(0), ""); + } + vm.stopBroadcast(); + + facets = loupe.facetAddresses(); + } + function getExcludes() internal virtual returns (bytes4[] memory) {} function getCallData() internal virtual returns (bytes memory) {} diff --git a/script/helperFunctions.sh b/script/helperFunctions.sh index dfba506b6..6f69a468f 100755 --- a/script/helperFunctions.sh +++ b/script/helperFunctions.sh @@ -866,37 +866,6 @@ function getConstructorArgsFromMasterLog() { return 0 } -function saveDiamond_DEPRECATED() { - :' - This contract version only saves the facet addresses as an array in the JSON file - without any further information (such as version or name, like in the new function) - ' - # read function arguments into variables - NETWORK=$1 - ENVIRONMENT=$2 - USE_MUTABLE_DIAMOND=$3 - FACETS=$4 - - # get file suffix based on value in variable ENVIRONMENT - local FILE_SUFFIX=$(getFileSuffix "$ENVIRONMENT") - - # store function arguments in variables - FACETS=$(echo "$4" | tr -d '[' | tr -d ']' | tr -d ',') - FACETS=$(printf '"%s",' "$FACETS" | sed 's/,*$//') - - # define path for json file based on which diamond was used - if [[ "$USE_MUTABLE_DIAMOND" == "true" ]]; then - DIAMOND_FILE="./deployments/${NETWORK}.diamond.${FILE_SUFFIX}json" - else - DIAMOND_FILE="./deployments/${NETWORK}.diamond.immutable.${FILE_SUFFIX}json" - fi - - # create an empty json if it does not exist - if [[ ! -e $DIAMOND_FILE ]]; then - echo "{}" >"$DIAMOND_FILE" - fi - jq -r ". + {\"facets\": [$FACETS] }" "$DIAMOND_FILE" >"${DIAMOND_FILE}.tmp" && mv "${DIAMOND_FILE}.tmp" "$DIAMOND_FILE" -} function saveDiamondFacets() { # read function arguments into variables local NETWORK=$1 @@ -904,8 +873,9 @@ function saveDiamondFacets() { local USE_MUTABLE_DIAMOND=$3 local FACETS=$4 # optional: output control for parallel orchestration - local OUTPUT_MODE="$5" # "facets-only" to write only facets JSON to OUTPUT_PATH - local OUTPUT_PATH="$6" # path to write facets JSON when in facets-only mode + local OUTPUT_MODE="$5" # "facets-only" to write only facets JSON to OUTPUT_PATH + local OUTPUT_PATH="$6" # path to write facets JSON when in facets-only mode + local FACETS_DIR="$7" # path for intermediate files, provided by the caller # logging for debug purposes echo "" @@ -914,31 +884,23 @@ function saveDiamondFacets() { echoDebug "ENVIRONMENT=$ENVIRONMENT" echoDebug "USE_MUTABLE_DIAMOND=$USE_MUTABLE_DIAMOND" echoDebug "FACETS=$FACETS" - - # get file suffix based on value in variable ENVIRONMENT - local FILE_SUFFIX=$(getFileSuffix "$ENVIRONMENT") - - # define path for json file based on which diamond was used - if [[ "$USE_MUTABLE_DIAMOND" == "true" ]]; then - DIAMOND_FILE="./deployments/${NETWORK}.diamond.${FILE_SUFFIX}json" - DIAMOND_NAME="LiFiDiamond" - else - DIAMOND_FILE="./deployments/${NETWORK}.diamond.immutable.${FILE_SUFFIX}json" - DIAMOND_NAME="LiFiDiamondImmutable" - fi + echoDebug "DIAMOND_CONTRACT_NAME=$DIAMOND_CONTRACT_NAME" + echoDebug "FACETS_DIR=$FACETS_DIR" # create an iterable FACETS array - # Remove brackets from FACETS string and split into array FACETS_ADJ="${4#\[}" FACETS_ADJ="${FACETS_ADJ%\]}" IFS=',' read -ra FACET_ADDRESSES <<<"$FACETS_ADJ" - # Set up a temp directory to collect facet entries (avoid concurrent writes) - local TEMP_DIR - TEMP_DIR=$(mktemp -d) - trap 'rm -rf "$TEMP_DIR" 2>/dev/null' EXIT - local FACETS_DIR="$TEMP_DIR/facets" - mkdir -p "$FACETS_DIR" + # ensure the directory for intermediate files exists + if [[ -n "$FACETS_DIR" ]]; then + mkdir -p "$FACETS_DIR" + else + # fallback for safety, for single-threaded operation where FACETS_DIR is not provided + # for parallel operation, the caller should always provide it + FACETS_DIR=$(mktemp -d) + trap 'rm -rf "$FACETS_DIR" 2>/dev/null' EXIT + fi # determine concurrency (fallback to 10 if not set) local CONCURRENCY=${MAX_CONCURRENT_JOBS:-10} @@ -963,9 +925,12 @@ function saveDiamondFacets() { JSON_ENTRY=$(findContractInMasterLogByAddress "$NETWORK" "$ENVIRONMENT" "$FACET_ADDRESS") if [[ $? -ne 0 || -z "$JSON_ENTRY" ]]; then warning "could not find any information about this facet address ($FACET_ADDRESS) in master log file while creating $DIAMOND_FILE (ENVIRONMENT=$ENVIRONMENT), " + # try to find name of contract from network-specific deployments file + # load JSON FILE that contains deployment addresses NAME=$(getContractNameFromDeploymentLogs "$NETWORK" "$ENVIRONMENT" "$FACET_ADDRESS") JSON_ENTRY="{\"$FACET_ADDRESS\": {\"Name\": \"$NAME\", \"Version\": \"\"}}" fi + # This now writes to the directory passed in by the parent function echo "$JSON_ENTRY" >"$FACETS_DIR/${FACET_ADDRESS}.json" ) & done @@ -999,16 +964,19 @@ function saveDiamondFacets() { fi # write merged facets to diamond file in a single atomic update - result=$(jq -r --arg diamond_name "$DIAMOND_NAME" --argjson facets_obj "$FACETS_JSON" ' + result=$(jq -r --arg diamond_name "$DIAMOND_CONTRACT_NAME" --argjson facets_obj "$FACETS_JSON" ' .[$diamond_name] = (.[$diamond_name] // {}) | .[$diamond_name].Facets = ((.[$diamond_name].Facets // {}) + $facets_obj) ' "$DIAMOND_FILE" || cat "$DIAMOND_FILE") printf %s "$result" >"$DIAMOND_FILE" - # add information about registered periphery contracts - saveDiamondPeriphery "$NETWORK" "$ENVIRONMENT" "$USE_MUTABLE_DIAMOND" + # add information about registered periphery contracts (only for regular diamonds, not LDA) + if [[ "$DIAMOND_CONTRACT_NAME" != "LiFiDEXAggregatorDiamond" ]]; then + saveDiamondPeriphery "$NETWORK" "$ENVIRONMENT" "$USE_MUTABLE_DIAMOND" + fi fi } + function saveDiamondPeriphery_MULTICALL_NOT_IN_USE() { # read function arguments into variables NETWORK=$1 @@ -1109,6 +1077,7 @@ function saveDiamondPeriphery() { # optional: output control for parallel orchestration local OUTPUT_MODE="$4" # "periphery-only" to write only periphery JSON to OUTPUT_PATH local OUTPUT_PATH="$5" # path to write periphery JSON when in periphery-only mode + local PERIPHERY_DIR="$6" # path for intermediate files, provided by the caller # get file suffix based on value in variable ENVIRONMENT local FILE_SUFFIX=$(getFileSuffix "$ENVIRONMENT") @@ -1146,11 +1115,13 @@ function saveDiamondPeriphery() { # get a list of all periphery contracts PERIPHERY_CONTRACTS=$(getContractNamesInFolder "src/Periphery/") - # prepare temp dir to collect per-contract JSON snippets - local TEMP_DIR - TEMP_DIR=$(mktemp -d) - local PERIPHERY_DIR="$TEMP_DIR/periphery" - mkdir -p "$PERIPHERY_DIR" + if [[ -n "$PERIPHERY_DIR" ]]; then + mkdir -p "$PERIPHERY_DIR" + else + PERIPHERY_DIR=$(mktemp -d) + # use a trap here as a fallback since it's self-contained if no dir is passed + trap 'rm -rf "$PERIPHERY_DIR" 2>/dev/null' EXIT + fi # determine concurrency (fallback to 10 if not set) local CONCURRENCY=${MAX_CONCURRENT_JOBS:-10} @@ -1206,10 +1177,8 @@ function saveDiamondPeriphery() { ' "$DIAMOND_FILE" || cat "$DIAMOND_FILE") printf %s "$result" >"$DIAMOND_FILE" fi - - # cleanup - rm -rf "$TEMP_DIR" } + function saveContract() { # read function arguments into variables local NETWORK=$1 @@ -2588,7 +2557,7 @@ function updateAllContractsToTargetState() { # update diamond with core facets echo "" - diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$DIAMOND_NAME" "UpdateCoreFacets" false 2>/dev/null + diamondUpdateFacet "$NETWORK" "$ENVIRONMENT" "$DIAMOND_NAME" "UpdateCoreFacets" 2>/dev/null # check if last command was executed successfully, otherwise exit script with error message checkFailure $? "update core facets in $DIAMOND_NAME on network $NETWORK" @@ -4454,6 +4423,12 @@ function updateDiamondLogForNetwork() { # read function arguments into variable local NETWORK=$1 local ENVIRONMENT=$2 + local DIAMOND_CONTRACT_NAME=$3 + + echoDebug "in function updateDiamondLogForNetwork" + echoDebug "NETWORK=$NETWORK" + echoDebug "ENVIRONMENT=$ENVIRONMENT" + echoDebug "DIAMOND_CONTRACT_NAME=$DIAMOND_CONTRACT_NAME" # get RPC URL local RPC_URL=$(getRPCUrl "$NETWORK") || checkFailure $? "get rpc url" @@ -4464,11 +4439,13 @@ function updateDiamondLogForNetwork() { fi # get diamond address - local DIAMOND_ADDRESS=$(getContractAddressFromDeploymentLogs "$NETWORK" "$ENVIRONMENT" "LiFiDiamond") + local DIAMOND_ADDRESS=$(getContractAddressFromDeploymentLogs "$NETWORK" "$ENVIRONMENT" "$DIAMOND_CONTRACT_NAME") - if [[ $? -ne 0 ]]; then - error "[$NETWORK] Failed to get LiFiDiamond address on $NETWORK in $ENVIRONMENT environment" - return 1 + echoDebug "DIAMOND_ADDRESS=$DIAMOND_ADDRESS" + + if [[ $? -ne 0 || -z "$DIAMOND_ADDRESS" ]]; then + warning "[$NETWORK] Failed to get $DIAMOND_CONTRACT_NAME address on $NETWORK in $ENVIRONMENT environment - contract may not be deployed. Skipping." + return 2 # Return a special exit code (2) for "skipped" instead of 0 (success) fi # get list of facets @@ -4479,7 +4456,6 @@ function updateDiamondLogForNetwork() { echo "[$NETWORK] Trying to get facets for diamond $DIAMOND_ADDRESS now - attempt $((attempts + 1))" # try to execute call local KNOWN_FACET_ADDRESSES=$(cast call "$DIAMOND_ADDRESS" "facetAddresses() returns (address[])" --rpc-url "$RPC_URL") 2>/dev/null - # check the return code the last call if [ $? -eq 0 ]; then break # exit the loop if the operation was successful @@ -4496,30 +4472,62 @@ function updateDiamondLogForNetwork() { # prepare for parallel facet/periphery processing and final merge local FILE_SUFFIX FILE_SUFFIX=$(getFileSuffix "$ENVIRONMENT") - local DIAMOND_FILE="./deployments/${NETWORK}.diamond.${FILE_SUFFIX}json" - local DIAMOND_NAME="LiFiDiamond" + + # Determine file path and settings based on diamond contract name + local DIAMOND_FILE + local USE_MUTABLE_DIAMOND + + if [[ "$DIAMOND_CONTRACT_NAME" == "LiFiDiamond" ]]; then + DIAMOND_FILE="./deployments/${NETWORK}.diamond.${FILE_SUFFIX}json" + USE_MUTABLE_DIAMOND="true" + elif [[ "$DIAMOND_CONTRACT_NAME" == "LiFiDiamondImmutable" ]]; then + DIAMOND_FILE="./deployments/${NETWORK}.diamond.immutable.${FILE_SUFFIX}json" + USE_MUTABLE_DIAMOND="false" + else + # LiFiDEXAggregatorDiamond + DIAMOND_FILE="./deployments/${NETWORK}.diamond.lda.${FILE_SUFFIX}json" + USE_MUTABLE_DIAMOND="false" + fi + + # create one isolated temp directory for this entire parallel operation local TEMP_DIR - TEMP_DIR=$(mktemp -d) - local FACETS_TMP="$TEMP_DIR/facets.json" - local PERIPHERY_TMP="$TEMP_DIR/periphery.json" + TEMP_DIR=$(mktemp -d -t "diamond_${DIAMOND_CONTRACT_NAME}_${NETWORK}_${ENVIRONMENT}_XXXXXX") + # ensure this directory is cleaned up when this function exits + trap 'rm -rf "$TEMP_DIR" 2>/dev/null' RETURN + + local FACETS_TMP="$TEMP_DIR/facets_${DIAMOND_CONTRACT_NAME}.json" + local PERIPHERY_TMP="$TEMP_DIR/periphery_${DIAMOND_CONTRACT_NAME}.json" + # define paths for the intermediate files within the isolated directory + local FACETS_SUBDIR="$TEMP_DIR/facets-intermediate" + local PERIPHERY_SUBDIR="$TEMP_DIR/periphery-intermediate" - # start periphery resolution in background - saveDiamondPeriphery "$NETWORK" "$ENVIRONMENT" "true" "periphery-only" "$PERIPHERY_TMP" & - local PID_PERIPHERY=$! + # export DIAMOND_CONTRACT_NAME for the functions to use + export DIAMOND_CONTRACT_NAME="$DIAMOND_CONTRACT_NAME" - # start facets resolution (if available) in background - local PID_FACETS + # start periphery and facets resolution in parallel + local pids=() + + # only process periphery for LiFiDiamond and LiFiDiamondImmutable, skip for LiFiDEXAggregatorDiamond + if [[ "$DIAMOND_CONTRACT_NAME" != "LiFiDEXAggregatorDiamond" ]]; then + saveDiamondPeriphery "$NETWORK" "$ENVIRONMENT" "$USE_MUTABLE_DIAMOND" "periphery-only" "$PERIPHERY_TMP" "$PERIPHERY_SUBDIR" & + pids+=($!) + fi + + # Start facets resolution (for all diamond types) if [[ -z $KNOWN_FACET_ADDRESSES ]]; then - warning "[$NETWORK] no facets found in diamond $DIAMOND_ADDRESS" + warning "[$NETWORK] no facets found in $DIAMOND_CONTRACT_NAME $DIAMOND_ADDRESS" echo '{}' >"$FACETS_TMP" else - saveDiamondFacets "$NETWORK" "$ENVIRONMENT" "true" "$KNOWN_FACET_ADDRESSES" "facets-only" "$FACETS_TMP" & - PID_FACETS=$! + saveDiamondFacets "$NETWORK" "$ENVIRONMENT" "$USE_MUTABLE_DIAMOND" "$KNOWN_FACET_ADDRESSES" "facets-only" "$FACETS_TMP" "$FACETS_SUBDIR" & + pids+=($!) fi # wait for both background jobs to complete - if [[ -n "$PID_PERIPHERY" ]]; then wait "$PID_PERIPHERY"; fi - if [[ -n "$PID_FACETS" ]]; then wait "$PID_FACETS"; fi + if [[ -n "${pids[*]}" ]]; then + for pid in "${pids[@]}"; do + wait "$pid" + done + fi # validate temp outputs exist if [[ ! -s "$FACETS_TMP" ]]; then echo '{}' >"$FACETS_TMP"; fi @@ -4532,17 +4540,20 @@ function updateDiamondLogForNetwork() { # merge facets and periphery into diamond file atomically local MERGED - MERGED=$(jq -r --arg diamond_name "$DIAMOND_NAME" --slurpfile facets "$FACETS_TMP" --slurpfile periphery "$PERIPHERY_TMP" ' + MERGED=$(jq -r --arg diamond_name "$DIAMOND_CONTRACT_NAME" --slurpfile facets "$FACETS_TMP" --slurpfile periphery "$PERIPHERY_TMP" ' .[$diamond_name] = (.[$diamond_name] // {}) | .[$diamond_name].Facets = $facets[0] | - .[$diamond_name].Periphery = $periphery[0] + # Conditionally add the Periphery key only if it is not the LDA diamond + if $diamond_name != "LiFiDEXAggregatorDiamond" then + .[$diamond_name].Periphery = $periphery[0] + else + . # Otherwise, pass the object through without adding the Periphery key + end ' "$DIAMOND_FILE" || cat "$DIAMOND_FILE") printf %s "$MERGED" >"$DIAMOND_FILE" - # clean up - rm -rf "$TEMP_DIR" - - success "[$NETWORK] updated diamond log" + # the trap automatically cleans up the TEMP_DIR + success "[$NETWORK] updated diamond log for $DIAMOND_CONTRACT_NAME" } function updateDiamondLogs() { @@ -4550,6 +4561,9 @@ function updateDiamondLogs() { local ENVIRONMENT=$1 local NETWORK=$2 + # Define all diamond contract names to process + local DIAMOND_CONTRACT_NAMES=("LiFiDiamond" "LiFiDEXAggregatorDiamond") + # if no network was passed to this function, update all networks if [[ -z $NETWORK ]]; then # get array with all network names @@ -4559,7 +4573,8 @@ function updateDiamondLogs() { fi echo "" - echo "Now updating all diamond logs on network(s): ${NETWORKS[*]}" + echo "Now updating all diamond logs for diamond contracts: ${DIAMOND_CONTRACT_NAMES[*]}" + echo "Networks: ${NETWORKS[*]}" echo "" # ENVIRONMENTS=("production" "staging") @@ -4574,7 +4589,7 @@ function updateDiamondLogs() { local job_info=() local job_index=0 - # loop through all networks + # Loop through all networks and environments for NETWORK in "${NETWORKS[@]}"; do echo "" echo "current Network: $NETWORK" @@ -4583,13 +4598,18 @@ function updateDiamondLogs() { echo " -----------------------" echo " current ENVIRONMENT: $ENVIRONMENT" - # Call the helper function in background for parallel execution - updateDiamondLogForNetwork "$NETWORK" "$ENVIRONMENT" & - - # Store the PID and job info - pids+=($!) - job_info+=("$NETWORK:$ENVIRONMENT") - job_index=$((job_index + 1)) + # Process all diamond contract names for this network/environment in parallel + for DIAMOND_CONTRACT_NAME in "${DIAMOND_CONTRACT_NAMES[@]}"; do + echo " Processing $DIAMOND_CONTRACT_NAME..." + + # Call the helper function in background for parallel execution + updateDiamondLogForNetwork "$NETWORK" "$ENVIRONMENT" "$DIAMOND_CONTRACT_NAME" & + + # Store the PID and job info + pids+=($!) + job_info+=("$NETWORK:$ENVIRONMENT:$DIAMOND_CONTRACT_NAME") + job_index=$((job_index + 1)) + done done done @@ -4601,16 +4621,22 @@ function updateDiamondLogs() { for i in "${!pids[@]}"; do local pid="${pids[$i]}" local info="${job_info[$i]}" + local exit_code=0 # Default to success - # Wait for this specific job and capture its exit code - if wait "$pid"; then + # wait for this specific job and then capture its exit code from the '$?' variable + wait "$pid" + exit_code=$? + + if [[ $exit_code -eq 0 ]]; then echo "[$info] Completed successfully" + elif [[ $exit_code -eq 2 ]]; then + # Handle our custom "skipped" code + echo "[$info] Skipped (contract not found in deployments)" else - echo "[$info] Failed with exit code $?" + echo "[$info] Failed with exit code $exit_code" failed_jobs+=("$info") fi done - # Check if any jobs failed if [ ${#failed_jobs[@]} -gt 0 ]; then error "Some diamond log updates failed: ${failed_jobs[*]}" diff --git a/script/removeDuplicateEventsFromABI.ts b/script/removeDuplicateEventsFromABI.ts index acd08cd96..b2f931205 100644 --- a/script/removeDuplicateEventsFromABI.ts +++ b/script/removeDuplicateEventsFromABI.ts @@ -1,21 +1,25 @@ /** - * This script cleans duplicate events from ABI files before typechain type generation. + * This script cleans duplicate events from ABI files and removes duplicate contract ABIs + * before typechain type generation. * * Background: * Due to our pattern inheritance structure in Solidity contracts, some events * are inherited multiple times through different paths, resulting in duplicate event - * definitions in the compiled ABI files. This causes typechain to generate duplicate + * definitions in the compiled ABI files. Additionally, some contracts exist in both + * main and draft versions (e.g., ERC20Permit in both lib/openzeppelin-contracts and + * lib/Permit2/lib/openzeppelin-contracts), causing typechain to generate duplicate * type definitions, leading to "Duplicate identifier" TypeScript errors. * * For example: * - OwnershipFacet.sol inherits from multiple interfaces that define the same events * - DiamondCutFacet.sol has similar inheritance patterns + * - ERC20Permit and IERC20Permit exist in both main and draft versions * * Solution: * Instead of modifying our contract inheritance structure, we clean the ABI files * after compilation but before typechain type generation. This script: - * 1. Reads specified ABI files - * 2. Removes duplicate events while preserving the first occurrence + * 1. Removes duplicate contract ABI files (draft versions that duplicate main versions) + * 2. Reads specified ABI files and removes duplicate events while preserving the first occurrence * 3. Saves the cleaned ABIs back to the files * * Usage: @@ -25,6 +29,7 @@ * 3. typechain --target ethers-v5 ... * * Note: + * - We remove draft versions of contracts that duplicate main versions * - We only clean specific files that we know have duplicate events * - The cleaning is based on event name and input parameters * - First occurrence of each event is preserved @@ -35,7 +40,7 @@ * ensures that the CI/CD pipeline can successfully generate and commit type bindings. */ -import { existsSync } from 'fs' +import { existsSync, unlinkSync } from 'fs' import { readFile, writeFile } from 'fs/promises' interface IEvent { @@ -46,6 +51,27 @@ interface IEvent { } async function removeDuplicateEventsFromABI() { + // Remove duplicate contract ABI files that cause duplicate type exports + // These are draft versions that duplicate the main versions + const duplicateContractFiles = [ + 'out/draft-ERC20Permit.sol/ERC20Permit.json', + 'out/draft-IERC20Permit.sol/IERC20Permit.json', + ] + + for (const file of duplicateContractFiles) { + if (existsSync(file)) { + try { + unlinkSync(file) + console.log(`Removed duplicate contract ABI: ${file}`) + } catch (error) { + console.error( + `Error removing ${file}:`, + error instanceof Error ? error.message : String(error) + ) + } + } + } + // Files that we know have duplicate events const filesToClean = [ 'out/OwnershipFacet.sol/OwnershipFacet.json', diff --git a/script/scriptMaster.sh b/script/scriptMaster.sh index 76bb942cb..d0805f22d 100755 --- a/script/scriptMaster.sh +++ b/script/scriptMaster.sh @@ -39,6 +39,7 @@ scriptMaster() { # load deploy script & helper functions source script/deploy/deploySingleContract.sh source script/deploy/deployAllContracts.sh + source script/deploy/deployAllLDAContracts.sh source script/helperFunctions.sh source script/deploy/deployFacetAndAddToDiamond.sh source script/deploy/deployPeripheryContracts.sh @@ -121,7 +122,8 @@ scriptMaster() { "10) Create updated target state from Google Docs (STAGING or PRODUCTION)" \ "11) Update diamond log(s)" \ "12) Propose upgrade TX to Gnosis SAFE" \ - "13) Remove facets or periphery from diamond" + "13) Remove facets or periphery from diamond" \ + "14) Deploy LDA contract to one selected network" \ ) #--------------------------------------------------------------------------------------------------------------------- @@ -145,27 +147,26 @@ scriptMaster() { checkRequiredVariablesInDotEnv "$NETWORK" # Handle ZkSync - # We need to make sure that the zksync fork of foundry is available before - # we can deploy contracts to zksync. if isZkEvmNetwork "$NETWORK"; then - # Use zksync specific scripts DEPLOY_SCRIPT_DIRECTORY="script/deploy/zksync/" # Check if the foundry-zksync binaries exist, if not fetch them install_foundry_zksync # get user-selected deploy script and contract from list SCRIPT=$(ls -1 "$DEPLOY_SCRIPT_DIRECTORY" | sed -e 's/\.zksync.s.sol$//' | grep 'Deploy' | gum filter --placeholder "Deploy Script") else - # get user-selected deploy script and contract from list + DEPLOY_SCRIPT_DIRECTORY="script/deploy/facets/" SCRIPT=$(ls -1 "$DEPLOY_SCRIPT_DIRECTORY" | sed -e 's/\.s.sol$//' | grep 'Deploy' | gum filter --placeholder "Deploy Script") fi # get user-selected deploy script and contract from list CONTRACT=$(echo "$SCRIPT" | sed -e 's/Deploy//') - # check if new contract should be added to diamond after deployment (only check for + # check if new contract should be added to diamond after deployment + # Skip diamond addition question for LiFiDEXAggregatorDiamond (has custom flow) and LiFiDiamond contracts if [[ ! "$CONTRACT" == "LiFiDiamond"* ]]; then echo "" echo "Do you want to add this contract to a diamond after deployment?" + # Check if this is an LDA contract based on network type and script ADD_TO_DIAMOND=$( gum choose \ "yes - to LiFiDiamond" \ @@ -181,14 +182,14 @@ scriptMaster() { if [[ "$ADD_TO_DIAMOND" == "yes"* ]]; then echo "[info] selected option: $ADD_TO_DIAMOND" - # determine the name of the LiFiDiamond contract and call helper function with correct diamond name + # determine the diamond type and call unified function if [[ "$ADD_TO_DIAMOND" == *"LiFiDiamondImmutable"* ]]; then - deployAndAddContractToDiamond "$NETWORK" "$ENVIRONMENT" "$CONTRACT" "LiFiDiamondImmutable" "$VERSION" + deployFacetAndAddToDiamond "$NETWORK" "$ENVIRONMENT" "$CONTRACT" "LiFiDiamondImmutable" "$VERSION" else - deployAndAddContractToDiamond "$NETWORK" "$ENVIRONMENT" "$CONTRACT" "LiFiDiamond" "$VERSION" + deployFacetAndAddToDiamond "$NETWORK" "$ENVIRONMENT" "$CONTRACT" "LiFiDiamond" "$VERSION" fi else - # just deploy the contract + # just deploy the contract (deploySingleContract will auto-detect LDA based on contract name) deploySingleContract "$CONTRACT" "$NETWORK" "$ENVIRONMENT" "" false fi @@ -555,6 +556,72 @@ scriptMaster() { elif [[ "$SELECTION" == "13)"* ]]; then bunx tsx script/tasks/cleanUpProdDiamond.ts + #--------------------------------------------------------------------------------------------------------------------- + # use case 14: Deploy LDA contract to one selected network + elif [[ "$SELECTION" == "14)"* ]]; then + echo "" + echo "[info] selected use case: Deploy LDA contract to one selected network" + + checkNetworksJsonFilePath || checkFailure $? "retrieve NETWORKS_JSON_FILE_PATH" + # get user-selected network from list + local NETWORK=$(jq -r 'keys[]' "$NETWORKS_JSON_FILE_PATH" | gum filter --placeholder "Network") + + echo "[info] selected network: $NETWORK" + echo "[info] loading deployer wallet balance..." + + # get deployer wallet balance + BALANCE=$(getDeployerBalance "$NETWORK" "$ENVIRONMENT") + + echo "[info] deployer wallet balance in this network: $BALANCE" + echo "" + checkRequiredVariablesInDotEnv "$NETWORK" + + # Handle ZkSync + if isZkEvmNetwork "$NETWORK"; then + # Use zksync specific LDA scripts + DEPLOY_SCRIPT_DIRECTORY="script/deploy/zksync/LDA/" + # Check if the foundry-zksync binaries exist, if not fetch them + install_foundry_zksync + # get user-selected deploy script and contract from list + SCRIPT=$(ls -1 "$DEPLOY_SCRIPT_DIRECTORY" | sed -e 's/\.zksync.s.sol$//' | grep 'Deploy' | gum filter --placeholder "Deploy Script") + else + # Use regular LDA scripts + DEPLOY_SCRIPT_DIRECTORY="script/deploy/facets/LDA/" + # get user-selected deploy script and contract from list + SCRIPT=$(ls -1 "$DEPLOY_SCRIPT_DIRECTORY" | sed -e 's/\.s.sol$//' | grep 'Deploy' | gum filter --placeholder "Deploy Script") + fi + + # get user-selected deploy script and contract from list + CONTRACT=$(echo "$SCRIPT" | sed -e 's/Deploy//') + + # check if new LDA facet should be added to LDA diamond after deployment + if [[ ! "$CONTRACT" == "LiFiDEXAggregatorDiamond"* ]]; then + echo "" + echo "Do you want to add this LDA facet to the LDA diamond after deployment?" + ADD_TO_DIAMOND=$( + gum choose \ + "yes - to LiFiDEXAggregatorDiamond" \ + " no - do not update any diamond" + ) + fi + + # get current contract version + local VERSION=$(getCurrentContractVersion "$CONTRACT") + + # check if contract should be added after deployment + if [[ "$ADD_TO_DIAMOND" == "yes"* ]]; then + echo "[info] selected option: $ADD_TO_DIAMOND" + # deploy LDA facet and add to LDA diamond using unified function + deployFacetAndAddToDiamond "$NETWORK" "$ENVIRONMENT" "$CONTRACT" "LiFiDEXAggregatorDiamond" "$VERSION" + else + # just deploy the LDA contract + deploySingleContract "$CONTRACT" "$NETWORK" "$ENVIRONMENT" "" false "true" + fi + + # check if last command was executed successfully, otherwise exit script with error message + checkFailure $? "deploy LDA contract $CONTRACT to network $NETWORK" + + #--------------------------------------------------------------------------------------------------------------------- else error "invalid use case selected ('$SELECTION') - exiting script" cleanup diff --git a/script/tasks/diamondUpdateFacet.sh b/script/tasks/diamondUpdateFacet.sh index 1243eb80b..0ad699c6d 100755 --- a/script/tasks/diamondUpdateFacet.sh +++ b/script/tasks/diamondUpdateFacet.sh @@ -12,7 +12,6 @@ diamondUpdateFacet() { local ENVIRONMENT="$2" local DIAMOND_CONTRACT_NAME="$3" local SCRIPT="$4" - local REPLACE_EXISTING_FACET="$5" # if no ENVIRONMENT was passed to this function, determine it if [[ -z "$ENVIRONMENT" ]]; then @@ -83,22 +82,37 @@ diamondUpdateFacet() { # if no SCRIPT was passed to this function, ask user to select it if [[ -z "$SCRIPT" ]]; then echo "Please select which facet you would like to update" - SCRIPT=$(ls -1 "$DEPLOY_SCRIPT_DIRECTORY" | sed -e 's/\.s.sol$//' | grep 'Update' | gum filter --placeholder "Update Script") + if [[ "$DIAMOND_CONTRACT_NAME" == "LiFiDEXAggregatorDiamond" ]]; then + SCRIPT=$(ls -1 "script/deploy/facets/LDA/" | sed -e 's/\.s.sol$//' | grep 'Update' | gum filter --placeholder "Update LDA Script") + else + SCRIPT=$(ls -1 "$DEPLOY_SCRIPT_DIRECTORY" | sed -e 's/\.s.sol$//' | grep 'Update' | gum filter --placeholder "Update Script") + fi fi - # Handle script paths and extensions based on network type + # Handle script paths and extensions based on network type and diamond type if isZkEvmNetwork "$NETWORK"; then - SCRIPT_PATH="script/deploy/zksync/$SCRIPT.zksync.s.sol" + if [[ "$DIAMOND_CONTRACT_NAME" == "LiFiDEXAggregatorDiamond" ]]; then + SCRIPT_PATH="script/deploy/zksync/LDA/$SCRIPT.zksync.s.sol" + else + SCRIPT_PATH="script/deploy/zksync/$SCRIPT.zksync.s.sol" + fi # Check if the foundry-zksync binaries exist, if not fetch them install_foundry_zksync else - SCRIPT_PATH=$DEPLOY_SCRIPT_DIRECTORY"$SCRIPT.s.sol" + if [[ "$DIAMOND_CONTRACT_NAME" == "LiFiDEXAggregatorDiamond" ]]; then + SCRIPT_PATH="script/deploy/facets/LDA/$SCRIPT.s.sol" + else + SCRIPT_PATH=$DEPLOY_SCRIPT_DIRECTORY"$SCRIPT.s.sol" + fi fi CONTRACT_NAME=$(basename "$SCRIPT_PATH" | sed 's/\.zksync\.s\.sol$//' | sed 's/\.s\.sol$//') # set flag for mutable/immutable diamond - USE_MUTABLE_DIAMOND=$([[ "$DIAMOND_CONTRACT_NAME" == "LiFiDiamond" ]] && echo true || echo false) + USE_MUTABLE_DIAMOND=$([[ "$DIAMOND_CONTRACT_NAME" == "LiFiDiamond" || "$DIAMOND_CONTRACT_NAME" == "LiFiDEXAggregatorDiamond" ]] && echo true || echo false) + + # set flag for LDA diamond + USE_LDA_DIAMOND=$([[ "$DIAMOND_CONTRACT_NAME" == "LiFiDEXAggregatorDiamond" ]] && echo false || echo true) # logging for debug purposes echoDebug "updating $DIAMOND_CONTRACT_NAME on $NETWORK with address $DIAMOND_ADDRESS in $ENVIRONMENT environment with script $SCRIPT (FILE_SUFFIX=$FILE_SUFFIX, USE_MUTABLE_DIAMOND=$USE_MUTABLE_DIAMOND)" @@ -122,10 +136,10 @@ diamondUpdateFacet() { if isZkEvmNetwork "$NETWORK"; then echo "zkEVM network detected" - RAW_RETURN_DATA=$(FOUNDRY_PROFILE=zksync NO_BROADCAST=true NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_MUTABLE_DIAMOND PRIVATE_KEY=$PRIVATE_KEY ./foundry-zksync/forge script "$SCRIPT_PATH" -f "$NETWORK" -vvvv --json --skip-simulation --slow --zksync --gas-limit 50000000) + RAW_RETURN_DATA=$(FOUNDRY_PROFILE=zksync NO_BROADCAST=true NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_LDA_DIAMOND PRIVATE_KEY=$PRIVATE_KEY ./foundry-zksync/forge script "$SCRIPT_PATH" -f "$NETWORK" -vvvv --json --skip-simulation --slow --zksync --gas-limit 50000000) else # PROD (normal mode): suggest diamondCut transaction to SAFE - RAW_RETURN_DATA=$(NO_BROADCAST=true NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_MUTABLE_DIAMOND PRIVATE_KEY=$PRIVATE_KEY forge script "$SCRIPT_PATH" -f "$NETWORK" -vvvv --json --skip-simulation --legacy) + RAW_RETURN_DATA=$(NO_BROADCAST=true NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_LDA_DIAMOND PRIVATE_KEY=$PRIVATE_KEY forge script "$SCRIPT_PATH" -f "$NETWORK" -vvvv --json --skip-simulation --legacy) fi # Extract JSON starting with {"logs": from mixed output @@ -163,9 +177,9 @@ diamondUpdateFacet() { echo "Sending diamondCut transaction directly to diamond (staging or new network deployment)..." if isZkEvmNetwork "$NETWORK"; then - RAW_RETURN_DATA=$(FOUNDRY_PROFILE=zksync NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_MUTABLE_DIAMOND ./foundry-zksync/forge script "$SCRIPT_PATH" -f "$NETWORK" --json --broadcast --skip-simulation --slow --zksync --gas-limit 50000000 --private-key $(getPrivateKey "$NETWORK" "$ENVIRONMENT")) + RAW_RETURN_DATA=$(FOUNDRY_PROFILE=zksync NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_LDA_DIAMOND ./foundry-zksync/forge script "$SCRIPT_PATH" -f "$NETWORK" --json --broadcast --skip-simulation --slow --zksync --gas-limit 50000000 --private-key $(getPrivateKey "$NETWORK" "$ENVIRONMENT")) else - RAW_RETURN_DATA=$(NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_MUTABLE_DIAMOND NO_BROADCAST=false PRIVATE_KEY=$(getPrivateKey "$NETWORK" "$ENVIRONMENT") forge script "$SCRIPT_PATH" -f "$NETWORK" -vvvv --json --broadcast --legacy) + RAW_RETURN_DATA=$(NETWORK=$NETWORK FILE_SUFFIX=$FILE_SUFFIX USE_DEF_DIAMOND=$USE_LDA_DIAMOND NO_BROADCAST=false PRIVATE_KEY=$(getPrivateKey "$NETWORK" "$ENVIRONMENT") forge script "$SCRIPT_PATH" -f "$NETWORK" -vvvv --json --broadcast --legacy) fi fi RETURN_CODE=$? @@ -203,7 +217,7 @@ diamondUpdateFacet() { # save facet addresses (only if deploying to staging, otherwise we update the logs after the diamondCut tx gets signed in the SAFE) if [[ "$ENVIRONMENT" != "production" ]]; then # Using default behavior: update diamond file (not facets-only mode) - saveDiamondFacets "$NETWORK" "$ENVIRONMENT" "$USE_MUTABLE_DIAMOND" "$FACETS" "" "" + saveDiamondFacets "$NETWORK" "$ENVIRONMENT" "$USE_MUTABLE_DIAMOND" "$FACETS" "" "" "" fi echo "[info] $SCRIPT successfully executed on network $NETWORK in $ENVIRONMENT environment" diff --git a/src/Facets/EmergencyPauseFacet.sol b/src/Facets/EmergencyPauseFacet.sol index 90172422a..7d134100b 100644 --- a/src/Facets/EmergencyPauseFacet.sol +++ b/src/Facets/EmergencyPauseFacet.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.17; import { LibDiamond } from "../Libraries/LibDiamond.sol"; import { LibDiamondLoupe } from "../Libraries/LibDiamondLoupe.sol"; -import { UnAuthorized, InvalidCallData, DiamondIsPaused } from "../Errors/GenericErrors.sol"; +import { UnAuthorized, InvalidCallData, DiamondIsPaused, InvalidConfig } from "../Errors/GenericErrors.sol"; import { IDiamondLoupe } from "lifi/Interfaces/IDiamondLoupe.sol"; import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; @@ -11,7 +11,7 @@ import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; /// @author LI.FI (https://li.fi) /// @notice Allows a LI.FI-owned and -controlled, non-multisig "PauserWallet" to remove a facet /// or pause the diamond in case of emergency -/// @custom:version 1.0.1 +/// @custom:version 1.0.2 /// @dev Admin-Facet for emergency purposes only contract EmergencyPauseFacet { /// Events /// @@ -51,6 +51,7 @@ contract EmergencyPauseFacet { /// Constructor /// /// @param _pauserWallet The address of the wallet that can execute emergency facet removal actions constructor(address _pauserWallet) { + if (_pauserWallet == address(0)) revert InvalidConfig(); pauserWallet = _pauserWallet; _emergencyPauseFacetAddress = address(this); } diff --git a/src/Helpers/ReentrancyGuard.sol b/src/Helpers/ReentrancyGuard.sol index bd80a433a..af5f87558 100644 --- a/src/Helpers/ReentrancyGuard.sol +++ b/src/Helpers/ReentrancyGuard.sol @@ -1,10 +1,10 @@ -// SPDX-License-Identifier: UNLICENSED -/// @custom:version 1.0.0 +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; /// @title Reentrancy Guard /// @author LI.FI (https://li.fi) /// @notice Abstract contract to provide protection against reentrancy +/// @custom:version 1.0.0 abstract contract ReentrancyGuard { /// Storage /// diff --git a/src/Helpers/WithdrawablePeriphery.sol b/src/Helpers/WithdrawablePeriphery.sol index 60b35cb2e..eb2e5b334 100644 --- a/src/Helpers/WithdrawablePeriphery.sol +++ b/src/Helpers/WithdrawablePeriphery.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: LGPL-3.0-only -/// @custom:version 1.0.0 pragma solidity ^0.8.17; import { TransferrableOwnership } from "./TransferrableOwnership.sol"; @@ -7,6 +6,10 @@ import { LibAsset } from "../Libraries/LibAsset.sol"; import { ExternalCallFailed } from "../Errors/GenericErrors.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +/// @title WithdrawablePeriphery +/// @author LI.FI (https://li.fi) +/// @notice Abstract contract to provide a withdraw function for tokens +/// @custom:version 1.0.0 abstract contract WithdrawablePeriphery is TransferrableOwnership { using SafeTransferLib for address; diff --git a/src/Interfaces/IAlgebraFactory.sol b/src/Interfaces/IAlgebraFactory.sol index 3ea12d70f..4a2f53a07 100644 --- a/src/Interfaces/IAlgebraFactory.sol +++ b/src/Interfaces/IAlgebraFactory.sol @@ -6,13 +6,11 @@ pragma solidity ^0.8.17; /// @author LI.FI (https://li.fi) /// @custom:version 1.0.0 interface IAlgebraFactory { - /** - * @notice Creates a pool for the given two tokens - * @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0 - * @param tokenA The contract address of either token0 or token1 - * @param tokenB The contract address of the other token - * @return pool The address of the newly created pool - */ + /// @notice Creates a pool for the given two tokens + /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0 + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @return pool The address of the newly created pool function createPool( address tokenA, address tokenB diff --git a/src/Interfaces/IAlgebraPool.sol b/src/Interfaces/IAlgebraPool.sol index 7cd38a7a0..25d1cc39c 100644 --- a/src/Interfaces/IAlgebraPool.sol +++ b/src/Interfaces/IAlgebraPool.sol @@ -6,17 +6,13 @@ pragma solidity ^0.8.17; /// @author LI.FI (https://li.fi) /// @custom:version 1.0.0 interface IAlgebraPool { - /** - * @notice The first of the two tokens of the pool, sorted by address - * @return The token contract address - */ + /// @notice The first of the two tokens of the pool, sorted by address + /// @return The token contract address function token0() external view returns (address); - /** - * @notice Sets the initial price for the pool - * @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value - * @param price the initial sqrt price of the pool as a Q64.96 - */ + /// @notice Sets the initial price for the pool + /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value + /// @param price the initial sqrt price of the pool as a Q64.96 function initialize(uint160 price) external; /// @notice Swaps tokens supporting fee on input tokens diff --git a/src/Interfaces/IAlgebraRouter.sol b/src/Interfaces/IAlgebraRouter.sol index 415017df7..237167d2d 100644 --- a/src/Interfaces/IAlgebraRouter.sol +++ b/src/Interfaces/IAlgebraRouter.sol @@ -5,13 +5,11 @@ pragma solidity ^0.8.17; /// @author LI.FI (https://li.fi) /// @custom:version 1.0.0 interface IAlgebraRouter { - /** - * @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist - * @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order - * @param tokenA The contract address of either token0 or token1 - * @param tokenB The contract address of the other token - * @return pool The pool address - */ + /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist + /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order + /// @param tokenA The contract address of either token0 or token1 + /// @param tokenB The contract address of the other token + /// @return pool The pool address function poolByPair( address tokenA, address tokenB diff --git a/src/Interfaces/ICurve.sol b/src/Interfaces/ICurve.sol new file mode 100644 index 000000000..1e9c5dad4 --- /dev/null +++ b/src/Interfaces/ICurve.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title Interface for Curve +/// @author LI.FI (https://li.fi) +/// @notice Minimal Curve pool interface for exchange operations +/// @custom:version 1.0.0 +interface ICurve { + /// @notice Performs a token swap on a Curve pool. + /// @dev This function is a minimal interface for the `exchange` function found on various Curve pools. + /// It is marked `payable` to allow for swaps involving native tokens (e.g., ETH) in some pools. + /// @param i The index of the token to sell. + /// @param j The index of the token to buy. + /// @param dx The amount of the input token to sell. + /// @param min_dy The minimum amount of the output token to receive (slippage control). + function exchange( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy + ) external payable; +} diff --git a/src/Interfaces/ICurveV2.sol b/src/Interfaces/ICurveV2.sol new file mode 100644 index 000000000..ef891bbcc --- /dev/null +++ b/src/Interfaces/ICurveV2.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title Interface for Curve V2 +/// @author LI.FI (https://li.fi) +/// @notice Minimal Curve V2 pool interface for exchange operations +/// @custom:version 1.0.0 +interface ICurveV2 { + /// @notice Performs a token swap on a Curve V2 pool. + /// @dev This function is used to swap tokens in a Curve V2 pool and specifies the recipient of the swapped tokens. + /// It is not `payable` and assumes that the input token has already been approved for transfer by the caller. + /// @param i The index of the token to sell. + /// @param j The index of the token to buy. + /// @param dx The amount of the input token to sell. + /// @param min_dy The minimum amount of the output token to receive (slippage control). + /// @param receiver The address to which the swapped tokens will be sent. + function exchange( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy, + address receiver + ) external; + + /// @param i The index of the token to sell. + /// @param j The index of the token to buy. + /// @param dx The amount of the input token to sell. + /// @param min_dy The minimum amount of the output token to receive (slippage control). + /// @param receiver The address to which the swapped tokens will be sent. + function exchange_received( + int128 i, + int128 j, + uint256 dx, + uint256 min_dy, + address receiver + ) external; +} diff --git a/src/Interfaces/IUniV2StylePool.sol b/src/Interfaces/IUniV2StylePool.sol new file mode 100644 index 000000000..e9f3ca2c1 --- /dev/null +++ b/src/Interfaces/IUniV2StylePool.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title IUniV2StylePool +/// @author LI.FI (https://li.fi) +/// @notice Interface for Uniswap V2 style pools (including SushiSwap, PancakeSwap V2, etc.) +/// @dev This interface represents the core functionality of AMMs that follow UniswapV2's pool design +/// Key characteristics: +/// - Uses x * y = k formula for pricing +/// - Maintains reserves for both tokens +/// - No callbacks during swaps (unlike V3-style pools) +/// @custom:version 1.0.0 +interface IUniV2StylePool { + /// @notice Returns the address of the token0 + function token0() external view returns (address); + /// @notice Returns the address of the token1 + function token1() external view returns (address); + /// @notice Returns the current reserves of the pool and the last block timestamp + /// @dev Values are stored as uint112 to fit into a single storage slot for gas optimization + /// @return reserve0 The reserve of token0 + /// @return reserve1 The reserve of token1 + /// @return blockTimestampLast The timestamp of the last block where reserves were updated + function getReserves() + external + view + returns ( + uint112 reserve0, + uint112 reserve1, + uint32 blockTimestampLast + ); + + /// @notice Executes a swap in the pool + /// @dev Unlike V3-style pools, this doesn't use callbacks - tokens must be sent to pool before swap + /// @param amount0Out The amount of token0 to send to recipient (0 if sending token1) + /// @param amount1Out The amount of token1 to send to recipient (0 if sending token0) + /// @param to The address that will receive the output tokens + /// @param data Optional data parameter, usually unused in V2-style pools + function swap( + uint256 amount0Out, + uint256 amount1Out, + address to, + bytes calldata data + ) external; +} diff --git a/src/Interfaces/IUniV3StylePool.sol b/src/Interfaces/IUniV3StylePool.sol new file mode 100644 index 000000000..57d84fde3 --- /dev/null +++ b/src/Interfaces/IUniV3StylePool.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title IUniV3StylePool +/// @author LI.FI (https://li.fi) +/// @notice Interface for UniV3-style pools +/// @custom:version 1.0.0 +interface IUniV3StylePool { + /// @notice Returns the address of the token0 + function token0() external view returns (address); + /// @notice Returns the address of the token1 + function token1() external view returns (address); + + /// @notice Swaps tokens + /// @param recipient The address of the recipient + /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 + /// @param amountSpecified The amount of the swap, positive for exact input, negative for exact output + /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit + /// @param data Any additional data required for the swap + /// @return amount0 The amount of token0 swapped + /// @return amount1 The amount of token1 swapped + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata data + ) external returns (int256 amount0, int256 amount1); +} diff --git a/src/Interfaces/IWETH.sol b/src/Interfaces/IWETH.sol new file mode 100644 index 000000000..ee6b0dc01 --- /dev/null +++ b/src/Interfaces/IWETH.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title IWETH +/// @author LI.FI (https://li.fi) +/// @notice Interface for WETH token +/// @custom:version 1.0.0 +interface IWETH { + /// @notice Deposit native ETH into the WETH contract + /// @dev This function is used to deposit native ETH into the WETH contract + function deposit() external payable; + + /// @notice Withdraw WETH to native ETH + /// @dev This function is used to withdraw WETH to native ETH + function withdraw(uint256) external; + + /// @notice Get the balance of an address + /// @dev This function is used to get the balance of an address + /// @return The balance of the address + function balanceOf(address) external view returns (uint256); +} diff --git a/src/LiFiDiamond.sol b/src/LiFiDiamond.sol index 49b571bbd..04d6e9a1e 100644 --- a/src/LiFiDiamond.sol +++ b/src/LiFiDiamond.sol @@ -3,15 +3,17 @@ pragma solidity ^0.8.17; import { LibDiamond } from "./Libraries/LibDiamond.sol"; import { IDiamondCut } from "./Interfaces/IDiamondCut.sol"; -// solhint-disable-next-line no-unused-import -import { LibUtil } from "./Libraries/LibUtil.sol"; +import { InvalidConfig } from "./Errors/GenericErrors.sol"; -/// @title LIFI Diamond +/// @title LiFiDiamond /// @author LI.FI (https://li.fi) /// @notice Base EIP-2535 Diamond Proxy Contract. -/// @custom:version 1.0.0 +/// @custom:version 1.0.1 contract LiFiDiamond { constructor(address _contractOwner, address _diamondCutFacet) payable { + if (_contractOwner == address(0)) { + revert InvalidConfig(); + } LibDiamond.setContractOwner(_contractOwner); // Add the diamondCut external function from the diamondCutFacet diff --git a/src/Libraries/LibAsset.sol b/src/Libraries/LibAsset.sol index 48aef5654..f50553313 100644 --- a/src/Libraries/LibAsset.sol +++ b/src/Libraries/LibAsset.sol @@ -4,8 +4,6 @@ pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LibSwap } from "./LibSwap.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; - -// solhint-disable-next-line max-line-length import { InvalidReceiver, NullAddrIsNotAValidSpender, InvalidAmount, NullAddrIsNotAnERC20Token } from "../Errors/GenericErrors.sol"; /// @title LibAsset diff --git a/src/Libraries/LibCallbackAuthenticator.sol b/src/Libraries/LibCallbackAuthenticator.sol new file mode 100644 index 000000000..ea2673529 --- /dev/null +++ b/src/Libraries/LibCallbackAuthenticator.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title Callback Manager Library +/// @author LI.FI (https://li.fi) +/// @notice Provides functionality for managing callback validation in diamond-safe storage +/// @custom:version 1.0.0 +library LibCallbackAuthenticator { + /// Types /// + bytes32 internal constant NAMESPACE = + keccak256("com.lifi.lda.callbackAuthenticator"); + + /// Storage /// + struct CallbackStorage { + address expected; + } + + /// Errors /// + error UnexpectedCallbackSender(address actual, address expected); + + /// @dev Fetch local storage + function callbackStorage() + internal + pure + returns (CallbackStorage storage cbStor) + { + bytes32 position = NAMESPACE; + // solhint-disable-next-line no-inline-assembly + assembly { + cbStor.slot := position + } + } + + /// @notice Arm the guard with expected pool + /// @param expectedCallbackSender The address expected to call the callback + function arm(address expectedCallbackSender) internal { + callbackStorage().expected = expectedCallbackSender; + } + + /// @notice Disarm the guard (called inside the callback) + function disarm() internal { + callbackStorage().expected = address(0); + } + + /// @notice Check that callback comes from expected address + function verifyCallbackSender() internal view { + address expected = callbackStorage().expected; + if (msg.sender != expected) { + revert UnexpectedCallbackSender(msg.sender, expected); + } + } +} diff --git a/src/Libraries/LibPackedStream.sol b/src/Libraries/LibPackedStream.sol new file mode 100644 index 000000000..6e7a70c57 --- /dev/null +++ b/src/Libraries/LibPackedStream.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title LibPackedStream +/// @author LI.FI (https://li.fi) +/// @notice A library for reading compact byte streams with minimal overhead +/// @dev Provides functions to read various integer types and addresses from a byte stream. +/// All integer reads are big-endian. The stream pointer advances after each read. +/// @custom:version 1.0.0 +library LibPackedStream { + /// @notice Returns the start and finish pointers for a bytes array + /// @param data The bytes array to get bounds for + /// @return start The pointer to the start of the actual bytes data (after length prefix) + /// @return finish The pointer to the end of the bytes data + function _bounds( + bytes memory data + ) private pure returns (uint256 start, uint256 finish) { + assembly { + start := add(data, 32) + finish := add(start, mload(data)) + } + } + + /// @notice Creates a new stream from a bytes array + /// @dev Allocates memory for stream pointers and initializes them + /// @param data The source bytes to create a stream from + /// @return stream A pointer to the stream struct (contains current position and end) + function createStream( + bytes memory data + ) internal pure returns (uint256 stream) { + (uint256 start, uint256 finish) = _bounds(data); + assembly { + stream := mload(0x40) + mstore(stream, start) + mstore(add(stream, 32), finish) + mstore(0x40, add(stream, 64)) + } + } + + /// @notice Checks if there are unread bytes in the stream + /// @param stream The stream to check + /// @return True if current position is before end of stream + function isNotEmpty(uint256 stream) internal pure returns (bool) { + uint256 pos; + uint256 finish; + assembly { + pos := mload(stream) + finish := mload(add(stream, 32)) + } + return pos < finish; + } + + /// @notice Reads a uint8 from the current stream position + /// @dev Reads 1 byte and advances stream by 1 + /// @param stream The stream to read from + /// @return res The uint8 value read + function readUint8(uint256 stream) internal pure returns (uint8 res) { + assembly { + let pos := mload(stream) + res := byte(0, mload(pos)) + mstore(stream, add(pos, 1)) + } + } + + /// @notice Reads a uint16 from the current stream position + /// @dev Reads 2 bytes big-endian and advances stream by 2 + /// @param stream The stream to read from + /// @return res The uint16 value read + function readUint16(uint256 stream) internal pure returns (uint16 res) { + assembly { + let pos := mload(stream) + res := shr(240, mload(pos)) + mstore(stream, add(pos, 2)) + } + } + + /// @notice Reads a uint24 from the current stream position + /// @dev Reads 3 bytes big-endian and advances stream by 3 + /// @param stream The stream to read from + /// @return res The uint24 value read + function readUint24(uint256 stream) internal pure returns (uint24 res) { + assembly { + let pos := mload(stream) + res := shr(232, mload(pos)) + mstore(stream, add(pos, 3)) + } + } + + /// @notice Reads a uint256 from the current stream position + /// @dev Reads 32 bytes and advances stream by 32 + /// @param stream The stream to read from + /// @return res The uint256 value read + function readUint256(uint256 stream) internal pure returns (uint256 res) { + assembly { + let pos := mload(stream) + res := mload(pos) + mstore(stream, add(pos, 32)) + } + } + + /// @notice Reads a bytes32 from the current stream position + /// @dev Reads 32 bytes and advances stream by 32 + /// @param stream The stream to read from + /// @return res The bytes32 value read + function readBytes32(uint256 stream) internal pure returns (bytes32 res) { + assembly { + let pos := mload(stream) + res := mload(pos) + mstore(stream, add(pos, 32)) + } + } + + /// @notice Reads an address from the current stream position + /// @dev Reads 20 bytes and advances stream by 20 + /// @param stream The stream to read from + /// @return res The address value read + function readAddress(uint256 stream) internal pure returns (address res) { + assembly { + let pos := mload(stream) + res := shr(96, mload(pos)) + mstore(stream, add(pos, 20)) + } + } + + /// @notice Reads a length-prefixed byte array from the stream + /// @dev Format: [uint16 length][bytes data]. Used for multi-hop routes where each hop's + /// data is prefixed with its length. Example of a 2-hop route encoding: + /// [uint16 hop1_len][bytes hop1_data][uint16 hop2_len][bytes hop2_data] + /// @param stream The stream to read from + /// @return res The bytes array read from the stream (without length prefix) + function readBytesWithLength( + uint256 stream + ) internal view returns (bytes memory res) { + // Read the 2-byte length prefix + uint16 len = LibPackedStream.readUint16(stream); + + if (len > 0) { + uint256 pos; + assembly { + pos := mload(stream) + } + assembly { + // Allocate memory for result + res := mload(0x40) + mstore(0x40, add(res, add(len, 32))) + // Store length and copy data + mstore(res, len) + pop(staticcall(gas(), 4, pos, len, add(res, 32), len)) + // Advance stream pointer + mstore(stream, add(pos, len)) + } + } + } +} diff --git a/src/Libraries/LibUniV3Logic.sol b/src/Libraries/LibUniV3Logic.sol new file mode 100644 index 000000000..e89d2aff0 --- /dev/null +++ b/src/Libraries/LibUniV3Logic.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { LibAsset } from "./LibAsset.sol"; + +/// @title UniV3 Logic Library +/// @author LI.FI (https://li.fi) +/// @notice Shared logic for UniV3-style DEX protocols +/// @custom:version 1.0.0 +library LibUniV3Logic { + using SafeERC20 for IERC20; + + /// @notice Handles a generic UniV3-style callback + /// @param amount0Delta The amount of token0 owed to pool + /// @param amount1Delta The amount of token1 owed to pool + /// @param data The callback data containing tokenIn address + function handleCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) internal { + int256 amount = amount0Delta > 0 ? amount0Delta : amount1Delta; + if (amount <= 0) { + return; // Nothing to pay + } + + address tokenIn = abi.decode(data, (address)); + LibAsset.transferERC20(tokenIn, msg.sender, uint256(amount)); + } +} diff --git a/src/Periphery/LDA/BaseRouteConstants.sol b/src/Periphery/LDA/BaseRouteConstants.sol new file mode 100644 index 000000000..0b742c57d --- /dev/null +++ b/src/Periphery/LDA/BaseRouteConstants.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +/// @title BaseRouteConstants +/// @author LI.FI (https://li.fi) +/// @notice Base contract providing common constants for DEX facets +/// @dev Abstract contract with shared constants to avoid duplication across facets +/// @custom:version 1.0.0 +abstract contract BaseRouteConstants { + /// @dev Constant indicating swap direction from token0 to token1 + uint8 internal constant DIRECTION_TOKEN0_TO_TOKEN1 = 1; + + /// @dev A sentinel address (address(1)) used in the `from` parameter of a swap. + /// It signals that the input tokens for the swap are already held by the + /// receiving contract (e.g., from a previous swap in a multi-step route). + /// This tells the facet to use its current token balance instead of + /// pulling funds from an external address via `transferFrom`. + address internal constant FUNDS_IN_RECEIVER = address(1); +} diff --git a/src/Periphery/LDA/Facets/AlgebraFacet.sol b/src/Periphery/LDA/Facets/AlgebraFacet.sol new file mode 100644 index 000000000..6a1a46df0 --- /dev/null +++ b/src/Periphery/LDA/Facets/AlgebraFacet.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibCallbackAuthenticator } from "lifi/Libraries/LibCallbackAuthenticator.sol"; +import { LibUniV3Logic } from "lifi/Libraries/LibUniV3Logic.sol"; +import { IAlgebraPool } from "lifi/Interfaces/IAlgebraPool.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { SwapCallbackNotExecuted } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; +import { PoolCallbackAuthenticator } from "lifi/Periphery/LDA/PoolCallbackAuthenticator.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; + +/// @title AlgebraFacet +/// @author LI.FI (https://li.fi) +/// @notice Handles Algebra swaps with callback management +/// @custom:version 1.0.0 +contract AlgebraFacet is BaseRouteConstants, PoolCallbackAuthenticator { + using LibPackedStream for uint256; + using SafeERC20 for IERC20; + + // ==== Constants ==== + /// @dev Minimum sqrt price ratio for Algebra pool swaps + uint160 internal constant MIN_SQRT_RATIO = 4295128739; + /// @dev Maximum sqrt price ratio for Algebra pool swaps + uint160 internal constant MAX_SQRT_RATIO = + 1461446703485210103287273052203988822378723970342; + + // ==== External Functions ==== + /// @notice Executes a swap through an Algebra pool + /// @dev Handles both regular swaps and fee-on-transfer token swaps + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, direction, destinationAddress, supportsFeeOnTransfer] + /// @param from Token source address - if equals msg.sender, + /// tokens will be pulled from the caller; otherwise assumes tokens are already at this contract + /// @param tokenIn Input token address + /// @param amountIn Amount of input tokens + function swapAlgebra( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; + address destinationAddress = stream.readAddress(); + bool supportsFeeOnTransfer = stream.readUint8() > 0; + + if ( + pool == address(0) || + destinationAddress == address(0) || + amountIn > uint256(type(int256).max) + ) revert InvalidCallData(); + + if (from == msg.sender) { + IERC20(tokenIn).safeTransferFrom( + msg.sender, + address(this), + uint256(amountIn) + ); + } + + LibCallbackAuthenticator.arm(pool); + + if (supportsFeeOnTransfer) { + IAlgebraPool(pool).swapSupportingFeeOnInputTokens( + address(this), + destinationAddress, + direction, + int256(amountIn), + direction ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, + abi.encode(tokenIn) + ); + } else { + IAlgebraPool(pool).swap( + destinationAddress, + direction, + int256(amountIn), + direction ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, + abi.encode(tokenIn) + ); + } + + if ( + LibCallbackAuthenticator.callbackStorage().expected != address(0) + ) { + revert SwapCallbackNotExecuted(); + } + } + + /// @notice Called by Algebra pool after executing a swap via IAlgebraPool#swap + /// @dev In the implementation you must pay the pool tokens owed for the swap. + /// The caller of this method must be verified to be an AlgebraPool using LibCallbackAuthenticator. + /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. + /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by + /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. + /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by + /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. + /// @param data Any data passed through by the caller via the IAlgebraPool#swap call. Contains the input token address. + function algebraSwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } +} diff --git a/src/Periphery/LDA/Facets/CoreRouteFacet.sol b/src/Periphery/LDA/Facets/CoreRouteFacet.sol new file mode 100644 index 000000000..180623a65 --- /dev/null +++ b/src/Periphery/LDA/Facets/CoreRouteFacet.sol @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibUtil } from "lifi/Libraries/LibUtil.sol"; +import { LibDiamondLoupe } from "lifi/Libraries/LibDiamondLoupe.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { ReentrancyGuard } from "lifi/Helpers/ReentrancyGuard.sol"; +import { InvalidReceiver } from "lifi/Errors/GenericErrors.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; + +/// @title CoreRouteFacet +/// @author LI.FI (https://li.fi) +/// @notice Orchestrates LDA route execution using direct function selector dispatch +/// @dev Implements selector-based routing where each DEX facet's swap function is called directly via its selector +/// @custom:version 1.0.0 +contract CoreRouteFacet is BaseRouteConstants, ReentrancyGuard { + using SafeERC20 for IERC20; + using SafeERC20 for IERC20Permit; + using LibPackedStream for uint256; + + // ==== Events ==== + event Route( + address indexed from, + address receiverAddress, + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOutMin, + uint256 amountOut + ); + + // ==== Errors ==== + error SwapTokenInSpendingExceeded( + uint256 actualSpent, + uint256 expectedSpent + ); + error SwapTokenOutAmountTooLow(uint256 actualOutput); + error UnknownCommandCode(); + error SwapFailed(); + error UnknownSelector(); + + // ==== External Functions ==== + /// @notice Process a route encoded with function selectors for direct DEX facet dispatch + /// @param tokenIn The input token address (address(0) for native) + /// @param amountIn The amount of input tokens + /// @param tokenOut The expected output token address (address(0) for native) + /// @param amountOutMin The minimum acceptable output amount + /// @param receiverAddress The receiver address for the output tokens + /// @param route The encoded route data containing function selectors and parameters + /// @return amountOut The actual amount of output tokens received + function processRoute( + address tokenIn, + uint256 amountIn, + address tokenOut, + uint256 amountOutMin, + address receiverAddress, + bytes calldata route + ) external payable nonReentrant returns (uint256 amountOut) { + if (receiverAddress == address(0)) revert InvalidReceiver(); + return + _executeRoute( + tokenIn, + amountIn, + tokenOut, + amountOutMin, + receiverAddress, + route + ); + } + + // ==== Private Functions - Core Logic ==== + /// @notice Executes a route with balance checks and event emission + /// @dev Handles both native and ERC20 tokens with pre/post balance validation + /// @param tokenIn The input token address (address(0) for native) + /// @param amountIn The amount of input tokens + /// @param tokenOut The expected output token address (address(0) for native) + /// @param amountOutMin The minimum acceptable output amount + /// @param receiverAddress The receiver address for the output tokens + /// @param route The encoded route data containing function selectors and parameters + /// @return amountOut The actual amount of output tokens received + function _executeRoute( + address tokenIn, + uint256 amountIn, + address tokenOut, + uint256 amountOutMin, + address receiverAddress, + bytes calldata route + ) private returns (uint256 amountOut) { + bool isNativeIn = LibAsset.isNativeAsset(tokenIn); + bool isNativeOut = LibAsset.isNativeAsset(tokenOut); + + // Get initial token balances, with special handling for native assets + (uint256 balInInitial, uint256 balOutInitial) = _getInitialBalances( + tokenIn, + tokenOut, + receiverAddress, + isNativeIn, + isNativeOut + ); + + // Execute the route and get actual input amount used (may differ from amountIn for some opcodes) + uint256 realAmountIn = _runRoute(tokenIn, amountIn, route); + + // Verify balances after route execution and calculate output amount + amountOut = _getFinalBalancesAndCheck( + tokenIn, + amountIn, + balInInitial, + tokenOut, + amountOutMin, + balOutInitial, + receiverAddress, + isNativeIn, + isNativeOut + ); + + emit Route( + msg.sender, + receiverAddress, + tokenIn, + tokenOut, + realAmountIn, + amountOutMin, + amountOut + ); + } + + /// @notice Gets initial balances for both input and output tokens before route execution + /// @dev For native input assets (ETH), we return 0 since: + /// 1. ETH balance checks would be misleading due to gas costs + /// 2. Native asset handling is done via _handleNative which consumes all ETH on the contract + /// @param tokenIn The input token address + /// @param tokenOut The output token address + /// @param receiverAddress The receiver address for output tokens + /// @param isNativeIn Whether input token is native ETH + /// @param isNativeOut Whether output token is native ETH + /// @return balInInitial Initial balance of input token + /// @return balOutInitial Initial balance of output token + function _getInitialBalances( + address tokenIn, + address tokenOut, + address receiverAddress, + bool isNativeIn, + bool isNativeOut + ) private view returns (uint256 balInInitial, uint256 balOutInitial) { + balInInitial = isNativeIn ? 0 : IERC20(tokenIn).balanceOf(msg.sender); + balOutInitial = isNativeOut + ? address(receiverAddress).balance + : IERC20(tokenOut).balanceOf(receiverAddress); + } + + function _getFinalBalancesAndCheck( + address tokenIn, + uint256 amountIn, + uint256 balInInitial, + address tokenOut, + uint256 amountOutMin, + uint256 balOutInitial, + address receiverAddress, + bool isNativeIn, + bool isNativeOut + ) private view returns (uint256 amountOut) { + uint256 balInFinal = isNativeIn + ? 0 + : IERC20(tokenIn).balanceOf(msg.sender); + if (balInFinal + amountIn < balInInitial) { + revert SwapTokenInSpendingExceeded( + balInFinal + amountIn, + balInInitial + ); + } + + uint256 balOutFinal = isNativeOut + ? address(receiverAddress).balance + : IERC20(tokenOut).balanceOf(receiverAddress); + amountOut = balOutFinal - balOutInitial; + if (amountOut < amountOutMin) { + revert SwapTokenOutAmountTooLow(amountOut); + } + } + + /// @notice Interprets and executes commands from the route byte stream + /// @dev A route is a packed byte stream of sequential commands. Each command begins with a 1-byte route command: + /// - 1 = DistributeSelfERC20 (distributes and swaps tokens already on this contract) + /// - 2 = DistributeUserERC20 (distributes and swaps tokens from user) + /// - 3 = DistributeNative (distributes and swaps ETH held by this contract) + /// - 4 = DispatchSinglePoolSwap (dispatches swap using tokens already in pool) + /// - 5 = ApplyPermit (EIP-2612 permit for tokenIn on behalf of msg.sender) + /// + /// Stream formats per route command: + /// 1. DistributeSelfERC20: + /// [1][token: address][n: uint8] then n legs, each: + /// [share: uint16][len: uint16][data: bytes] + /// total = IERC20(token).balanceOf(address(this)) minus 1 wei (prevents tiny swaps) + /// from = address(this), tokenIn = token + /// + /// 2. DistributeUserERC20: + /// [2][token: address][n: uint8] then n legs, each: + /// [share: uint16][len: uint16][data: bytes] + /// total = declaredAmountIn + /// from = msg.sender, tokenIn = token + /// + /// 3. DistributeNative: + /// [3][n: uint8] then n legs, each: + /// [share: uint16][len: uint16][data: bytes] + /// total = address(this).balance (includes msg.value and any residual ETH) + /// from = address(this), tokenIn is native ETH (address(0)) + /// + /// 4. DispatchSinglePoolSwap: + /// [4][token: address][len: uint16][data: bytes] + /// amountIn = 0 (pool sources tokens internally), from = FUNDS_IN_RECEIVER + /// + /// 5. ApplyPermit: + /// [5][value: uint256][deadline: uint256][v: uint8][r: bytes32][s: bytes32] + /// Calls permit on tokenIn for msg.sender -> address(this). No swap occurs. + /// + /// Leg data encoding: + /// Each leg's data field contains [selector (4 bytes) | payload (bytes)]. + /// The selector determines the DEX facet function to call. The router delegatecalls the facet with: + /// (bytes swapData, address from, address tokenIn, uint256 amountIn) + /// where swapData is the payload from the route, containing DEX-specific data: + /// - Example for UniV3-style: abi.encode(pool, direction, destinationAddress) // for Uniswap V3, PancakeV3, etc. + /// - Each DEX facet defines its own payload format based on what its pools need + /// + /// Example multihop route (two legs on user ERC20, then single-pool hop): + /// ``` + /// // Leg payloads with facet selectors: + /// leg1 = abi.encodePacked( + /// UniV3StyleFacet.swapUniV3.selector, + /// abi.encode(poolA, DIRECTION_TOKEN0_TO_TOKEN1, poolC) // destinationAddress is the final pool + /// ); + /// leg2 = abi.encodePacked( + /// IzumiV3Facet.swapIzumiV3.selector, + /// abi.encode(poolB, DIRECTION_TOKEN0_TO_TOKEN1, poolC) // destinationAddress is the final pool + /// ); + /// leg3 = abi.encodePacked( + /// SomePoolFacet.swapSinglePool.selector, + /// abi.encode(poolC, finalReceiver, otherPoolParams) // pool that received tokens from leg1&2 + /// ); + /// + /// // Full route: [2][tokenA][2 legs][60% leg1][40% leg2] then [4][tokenB][leg3] + /// route = abi.encodePacked( + /// uint8(2), tokenA, uint8(2), // DistributeUserERC20 with 2 legs + /// uint16(39321), uint16(leg1.length), leg1, // ~60% of amountIn + /// uint16(26214), uint16(leg2.length), leg2, // ~40% of amountIn + /// uint8(4), tokenB, // DispatchSinglePoolSwap + /// uint16(leg3.length), leg3 + /// ); + /// ``` + /// @param tokenIn The input token address + /// @param declaredAmountIn The declared input amount + /// @param route The encoded route data + /// @return realAmountIn The actual amount used in the first hop + function _runRoute( + address tokenIn, + uint256 declaredAmountIn, + bytes calldata route + ) private returns (uint256 realAmountIn) { + realAmountIn = declaredAmountIn; + uint256 step = 0; + + uint256 stream = LibPackedStream.createStream(route); + // Iterate until the packed route stream is fully consumed. + // `isNotEmpty()` returns true while there are unread bytes left in the stream. + while (stream.isNotEmpty()) { + // Read the next command byte that specifies how to handle tokens in this step + uint8 routeCommand = stream.readUint8(); + if (routeCommand == 1) { + uint256 used = _distributeSelfERC20(stream); + if (step == 0) realAmountIn = used; + } else if (routeCommand == 2) { + _distributeUserERC20(stream, declaredAmountIn); + } else if (routeCommand == 3) { + uint256 usedNative = _distributeNative(stream); + if (step == 0) realAmountIn = usedNative; + } else if (routeCommand == 4) { + _dispatchSinglePoolSwap(stream); + } else if (routeCommand == 5) { + _applyPermit(tokenIn, stream); + } else { + revert UnknownCommandCode(); + } + unchecked { + ++step; + } + } + } + + // ==== Private Functions ==== + + /// @notice Applies ERC20 permit for token approval + /// @dev Reads permit parameters from the stream and calls permit on the token + /// @param tokenIn The token to approve + /// @param stream The byte stream to read from + function _applyPermit(address tokenIn, uint256 stream) private { + uint256 value = stream.readUint256(); + uint256 deadline = stream.readUint256(); + uint8 v = stream.readUint8(); + bytes32 r = stream.readBytes32(); + bytes32 s = stream.readBytes32(); + IERC20Permit(tokenIn).safePermit( + msg.sender, + address(this), + value, + deadline, + v, + r, + s + ); + } + + /// @notice Distributes native ETH held by this contract across legs and dispatches swaps + /// @dev Assumes ETH is already present on the contract + /// @param stream The byte stream to read from + /// @return total The total amount of ETH to process + function _distributeNative( + uint256 stream + ) private returns (uint256 total) { + total = address(this).balance; + _distributeAndSwap( + stream, + address(this), + LibAsset.NULL_ADDRESS, + total + ); + } + + /// @notice Distributes ERC20 tokens already on this contract + /// @dev Includes protection against full balance draining + /// @param stream The byte stream to read from + /// @return total The total amount of tokens to process + function _distributeSelfERC20( + uint256 stream + ) private returns (uint256 total) { + address token = stream.readAddress(); + total = IERC20(token).balanceOf(address(this)); + unchecked { + // Prevent swaps with uselessly small amounts (like 1 wei) that could: + // 1. Cause the entire transaction to fail (most DEXs reject such tiny trades) + // 2. Waste gas even if they succeeded + // By subtracting 1 from any positive balance, we ensure a balance of 1 becomes a swap amount of 0 (effectively skipping the swap) + if (total > 0) total -= 1; + } + _distributeAndSwap(stream, address(this), token, total); + } + + /// @notice Distributes ERC20 tokens from the caller + /// @param stream The byte stream to read from + /// @param total The declared total to distribute from msg.sender + function _distributeUserERC20(uint256 stream, uint256 total) private { + address token = stream.readAddress(); + _distributeAndSwap(stream, msg.sender, token, total); + } + + /// @notice Dispatches a single swap using tokens already in the pool + /// @param stream The byte stream to read from + function _dispatchSinglePoolSwap(uint256 stream) private { + address token = stream.readAddress(); + _dispatchSwap(stream, FUNDS_IN_RECEIVER, token, 0); + } + + /// @notice Distributes tokens across multiple pools based on share ratios + /// @dev This function implements proportional distribution where: + /// - Each leg gets a percentage of the total based on its share value + /// - Shares are encoded as uint16 where 65535 (type(uint16).max) = 100% + /// - The last leg gets all remaining tokens to handle rounding errors + /// - Example: 60/40 split would use shares [39321, 26214] since: + /// 39321/65535 ≈ 0.6 and 26214/65535 ≈ 0.4 + /// @param stream The byte stream to read from + /// @param from The source address for tokens + /// @param tokenIn The token being distributed + /// @param total The total amount to distribute across all legs + function _distributeAndSwap( + uint256 stream, + address from, + address tokenIn, + uint256 total + ) private { + // Read number of swap legs from the stream + uint8 n = stream.readUint8(); + unchecked { + uint256 remaining = total; + for (uint256 i = 0; i < n; ++i) { + // Read the proportional share for this leg (0-65535 scale) + uint16 share = stream.readUint16(); + + // Calculate amount for this leg: + // - For intermediate legs: proportional amount based on share + // - For last leg: all remaining tokens (handles rounding dust) + uint256 legAmount = i == n - 1 + ? remaining // Last leg gets all remaining to avoid dust + : (total * share) / type(uint16).max; // Proportional calculation + + // Safety check: never exceed what's left to distribute + if (legAmount > remaining) legAmount = remaining; + + // Update remaining balance for next iteration + remaining -= legAmount; + + // Execute the swap for this leg with calculated amount + _dispatchSwap(stream, from, tokenIn, legAmount); + } + } + } + + /// @notice Dispatches a swap call to the appropriate DEX facet + /// @param stream The byte stream to read from + /// @param from The source address for tokens + /// @param tokenIn The input token address + /// @param amountIn The amount of tokens to swap + function _dispatchSwap( + uint256 stream, + address from, + address tokenIn, + uint256 amountIn + ) private { + // Read [selector | payload] blob for the specific DEX facet + bytes memory data = stream.readBytesWithLength(); + + // Extract function selector (first 4 bytes of data) + bytes4 selector = _readSelector(data); + + // Calculate payload length by subtracting selector size from total data length + // data memory layout: [length][selector(4 bytes)][payload...] + // mload(data) reads the length field, then we subtract 4 bytes for the selector + uint256 payloadLen; + assembly { + payloadLen := sub(mload(data), 4) + } + + address facet = LibDiamondLoupe.facetAddress(selector); + if (facet == address(0)) revert UnknownSelector(); + + bool success; + bytes memory returnData; + assembly { + // Example: Building calldata for swapUniV3(bytes,address,address,uint256) + // with example values: + // - selector: 0x1234abcd + // - from: 0xaaa... + // - tokenIn: 0xbbb... (USDC) + // - amountIn: 1000000 (1 USDC) + // - swapData: abi.encode( + // pool: 0x123..., + // direction: 1, + // destinationAddress: 0x456... + // ) + // + // Memory layout it builds (each line is 32 bytes): + // Position Content + // 0x80: 0x1234abcd00000000... // selector + // 0x84: 0x80 // offset to swapData + // 0xa4: 0xaaa... // from address + // 0xc4: 0xbbb... // tokenIn address + // 0xe4: 0x0f4240 // amountIn (1000000) + // 0x104: 0x60 // swapData length (96) + // 0x124: 0x123... // pool address + // 0x144: 0x01 // direction + // 0x164: 0x456... // destinationAddress + + // Free memory pointer where we’ll build calldata for delegatecall + let free := mload(0x40) + + // Calldata layout we build: + // [0..3] function selector + // [4..] ABI-encoded args: + // head (4 words): + // word0: offset_to_payload (0x80 = after 4 static words) + // word1: from (address) + // word2: tokenIn (address) + // word3: amountIn (uint256) + // payload area: + // word4: payload.length + // word5+: payload bytes (padded to 32 bytes) + + // Write function selector + mstore(free, selector) + let args := add(free, 4) + + // Head: [offset_to_payload, from, tokenIn, amountIn] + mstore(args, 0x80) // offset to payload data + mstore(add(args, 0x20), from) + mstore(add(args, 0x40), tokenIn) + mstore(add(args, 0x60), amountIn) + + // Payload area (length + bytes) + let d := add(args, 0x80) + let len := payloadLen + mstore(d, len) + + // Copy from data + 32 (skip length) + 4 (skip selector) + // to = d+32; from = data+36; size = len + pop(staticcall(gas(), 0x04, add(data, 36), len, add(d, 32), len)) + + // Round payload length up to a multiple of 32 and compute total calldata size + let padded := and(add(len, 31), not(31)) + // total = 4 (selector) + 0x80 (head) + 0x20 (payload length) + padded payload + let total := add(4, add(0x80, add(0x20, padded))) + + // Perform delegatecall into facet. + // delegatecall preserves msg.sender and storage context (diamond pattern). + success := delegatecall(gas(), facet, free, total, 0, 0) + + // Advance the free memory pointer past our calldata buffer (even on failure). + mstore(0x40, add(free, total)) + + // Only allocate/copy return data on failure to save gas on success. + switch success + case 0 { + let rsize := returndatasize() + returnData := mload(0x40) + mstore(returnData, rsize) + let rptr := add(returnData, 32) + returndatacopy(rptr, 0, rsize) + // bump free memory pointer past the return data buffer (padded) + mstore(0x40, add(rptr, and(add(rsize, 31), not(31)))) + } + } + + // Bubble up revert data if delegatecall failed + if (!success) { + LibUtil.revertWith(returnData); + } + } + + // ==== Private Functions - Helpers ==== + + /// @notice Extracts function selector from calldata + /// @dev Assembly used to load the first 4 bytes (selector) directly from the bytes blob: + /// - bytes are laid out as [len (32 bytes) | data...] + /// - mload(add(blob, 32)) loads the first 32 bytes of data + /// - Solidity ABI selectors occupy the first 4 bytes + /// @param blob The calldata bytes + /// @return sel The extracted selector + function _readSelector( + bytes memory blob + ) private pure returns (bytes4 sel) { + assembly { + sel := mload(add(blob, 32)) + } + } + + /// @notice Creates a new bytes view that aliases blob without the first 4 bytes (selector) + /// @param blob The original calldata bytes + /// @return payload The calldata without selector + function _payloadFrom( + bytes memory blob + ) private pure returns (bytes memory payload) { + assembly { + // Point payload 4 bytes into blob's data section (skipping selector) + // Memory layout: [length][data...] -> payload points to [data+4...] + payload := add(blob, 4) + // Update length field: original_length - 4 (selector size) + // This creates a valid bytes object that references blob's memory + mstore(payload, sub(mload(blob), 4)) + } + } +} diff --git a/src/Periphery/LDA/Facets/CurveFacet.sol b/src/Periphery/LDA/Facets/CurveFacet.sol new file mode 100644 index 000000000..572e53730 --- /dev/null +++ b/src/Periphery/LDA/Facets/CurveFacet.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { ICurve } from "lifi/Interfaces/ICurve.sol"; +import { ICurveV2 } from "lifi/Interfaces/ICurveV2.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseRouteConstants } from "lifi/Periphery/LDA/BaseRouteConstants.sol"; + +/// @title CurveFacet +/// @author LI.FI (https://li.fi) +/// @notice Handles swaps across all Curve pool variants (Legacy, Factory, NG) +/// @dev +/// Pool Types & Interface Selection: +/// 1. Legacy Pools (isV2 = false): +/// - Version <= 0.2.4 (like 3pool, compound, ETH/stETH etc.) +/// - Uses 4-arg exchange(i, j, dx, min_dy) +/// - No receiver param, always sends to msg.sender +/// - Native ETH Support: +/// * When selling ETH (i == 0): Accepts msg.value as input +/// * When buying ETH (j == 0): Returns native ETH via raw_call +/// - We must transfer output tokens manually to destinationAddress +/// +/// 2. Modern Pools (isV2 = true): +/// - Factory pools and StableNG pools +/// - Uses 5-arg exchange(i, j, dx, min_dy, receiver) +/// - Direct transfer to specified receiver +/// - For NG pools only: supports optimistic swap via exchange_received +/// when from == address(0) signals tokens were pre-sent +/// - Does not support pure native ETH (uses wrapped versions) +/// @custom:version 1.0.0 +contract CurveFacet is BaseRouteConstants { + using LibPackedStream for uint256; + using LibAsset for IERC20; + + // ==== External Functions ==== + /// @notice Executes a swap through a Curve pool + /// @dev + /// - swapData is tightly packed as: + /// [ pool: address, + /// isV2: uint8 (0 = 4-arg legacy/main, 1 = 5-arg factory/NG), + /// fromIndex: uint8, + /// toIndex: uint8, + /// destinationAddress: address (receiver for 5-arg; post-transfer target for 4-arg), + /// tokenOut: address ] + /// - `from` controls token sourcing: + /// - if `from == msg.sender` and amountIn > 0 - facet pulls `amountIn` from caller, + /// - if `from != msg.sender` - tokens are assumed to be already available (e.g., previous hop). + /// Special case (NG optimistic): if `isV2 == 1` and `from == FUNDS_IN_RECEIVER` (address(1)), the facet calls + /// `exchange_received` on NG pools (tokens must have been pre-sent to the pool). + /// - Indices (i,j) must match the pool's coin ordering. + /// - Native ETH handling: + /// * For legacy pools (isV2 = false): + /// - When tokenIn is native: msg.value must equal amountIn + /// - When tokenOut is native: balance tracking uses address.balance + /// * For modern pools (isV2 = true): + /// - Native ETH not supported, use wrapped versions + /// @param swapData Encoded swap parameters [pool, isV2, fromIndex, toIndex, destinationAddress, tokenOut] + /// @param from Token source address; if equals msg.sender, tokens will be pulled; + /// if set to FUNDS_IN_RECEIVER (address(1)) with isV2==1, signals NG optimistic hop (tokens pre-sent) + /// @param tokenIn Input token address (address(0) for native ETH in legacy pools) + /// @param amountIn Amount of input tokens (ignored for NG optimistic hop) + function swapCurve( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool isV2 = stream.readUint8() > 0; // Convert uint8 to bool. 0 = V1, 1 = V2 + int128 fromIndex = int128(uint128(stream.readUint8())); + int128 toIndex = int128(uint128(stream.readUint8())); + address destinationAddress = stream.readAddress(); + address tokenOut = stream.readAddress(); + + if (pool == address(0) || destinationAddress == address(0)) + revert InvalidCallData(); + + if (from == msg.sender) { + LibAsset.depositAsset(tokenIn, amountIn); + } + + bool isNativeOut = LibAsset.isNativeAsset(tokenOut); + bool isNativeIn = LibAsset.isNativeAsset(tokenIn); + if (!isNativeIn) { + LibAsset.maxApproveERC20(IERC20(tokenIn), pool, amountIn); + } + + // Only track balances for legacy path that needs manual transfer. Legacy pools doesn't have receiver param and always sends tokenOut to msg.sender + uint256 balanceBefore; + if (!isV2 && destinationAddress != address(this)) { + balanceBefore = isNativeOut + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); + } + + if (isV2) { + if (isNativeIn) revert InvalidCallData(); + if (from == FUNDS_IN_RECEIVER) { + // Optimistic NG hop: tokens already sent to pool by previous hop. + // NG requires _dx > 0 and asserts actual delta >= _dx. + ICurveV2(pool).exchange_received( + fromIndex, + toIndex, + 1, // minimal positive hint + 0, + destinationAddress + ); + } else { + // Modern pools do not use pure native ETH path. They use WETH instead + ICurveV2(pool).exchange( + fromIndex, + toIndex, + amountIn, + 0, + destinationAddress + ); + } + } else { + // Legacy pools can accept/return native ETH + ICurve(pool).exchange{ value: isNativeIn ? amountIn : 0 }( + fromIndex, + toIndex, + amountIn, + 0 + ); + } + + // Only transfer when legacy path kept tokens on this contract + if (!isV2 && destinationAddress != address(this)) { + uint256 balanceAfter = isNativeOut + ? address(this).balance + : IERC20(tokenOut).balanceOf(address(this)); + + LibAsset.transferAsset( + tokenOut, + payable(destinationAddress), + balanceAfter - balanceBefore + ); + } + } +} diff --git a/src/Periphery/LDA/Facets/IzumiV3Facet.sol b/src/Periphery/LDA/Facets/IzumiV3Facet.sol new file mode 100644 index 000000000..aa1bb4dcc --- /dev/null +++ b/src/Periphery/LDA/Facets/IzumiV3Facet.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { LibCallbackAuthenticator } from "lifi/Libraries/LibCallbackAuthenticator.sol"; +import { IiZiSwapPool } from "lifi/Interfaces/IiZiSwapPool.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { SwapCallbackNotExecuted } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; +import { PoolCallbackAuthenticator } from "lifi/Periphery/LDA/PoolCallbackAuthenticator.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; + +/// @title IzumiV3Facet +/// @author LI.FI (https://li.fi) +/// @notice Handles IzumiV3 swaps with callback management +/// @custom:version 1.0.0 +contract IzumiV3Facet is BaseRouteConstants, PoolCallbackAuthenticator { + using LibPackedStream for uint256; + using LibCallbackAuthenticator for *; + + // ==== Constants ==== + /// @dev Minimum point boundary for iZiSwap pool price range + int24 internal constant IZUMI_LEFT_MOST_PT = -800000; + /// @dev Maximum point boundary for iZiSwap pool price range + int24 internal constant IZUMI_RIGHT_MOST_PT = 800000; + + // ==== Errors ==== + /// @dev Thrown when callback amount to pay is zero + error IzumiV3SwapCallbackNotPositiveAmount(); + + // ==== External Functions ==== + /// @notice Executes a swap through an iZiSwap V3 pool + /// @dev Handles both X to Y and Y to X swaps with callback verification + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, direction, destinationAddress] + /// @param from Token source address - if equals msg.sender, tokens will be pulled from the caller + /// @param tokenIn Input token address + /// @param amountIn Amount of input tokens + function swapIzumiV3( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; // 0 = Y2X, 1 = X2Y + address destinationAddress = stream.readAddress(); + + if ( + pool == address(0) || + destinationAddress == address(0) || + amountIn > type(uint128).max + ) revert InvalidCallData(); + + if (from == msg.sender) { + LibAsset.transferFromERC20( + tokenIn, + msg.sender, + address(this), + amountIn + ); + } + + LibCallbackAuthenticator.arm(pool); + + if (direction) { + IiZiSwapPool(pool).swapX2Y( + destinationAddress, + uint128(amountIn), + IZUMI_LEFT_MOST_PT + 1, + abi.encode(tokenIn) + ); + } else { + IiZiSwapPool(pool).swapY2X( + destinationAddress, + uint128(amountIn), + IZUMI_RIGHT_MOST_PT - 1, + abi.encode(tokenIn) + ); + } + + // After the swapX2Y or swapY2X call, the callback should clear the registered pool + // If it hasn't, it means the callback either didn't happen, was incorrect, or the pool misbehaved + // so we revert to protect against misuse or faulty integrations + if ( + LibCallbackAuthenticator.callbackStorage().expected != address(0) + ) { + revert SwapCallbackNotExecuted(); + } + } + + // ==== Callback Functions ==== + /// @notice Callback for X to Y swaps from iZiSwap pool + /// @dev Verifies callback source and handles token transfer + /// @param amountX Amount of token0 that must be sent to the pool + /// @param data Encoded data containing input token address + function swapX2YCallback( + uint256 amountX, + uint256, + bytes calldata data + ) external onlyExpectedPool { + _handleIzumiV3SwapCallback(amountX, data); + } + + /// @notice Callback for Y to X swaps from iZiSwap pool + /// @dev Verifies callback source and handles token transfer + /// @param amountY Amount of token1 that must be sent to the pool + /// @param data Encoded data containing input token address + function swapY2XCallback( + uint256, + uint256 amountY, + bytes calldata data + ) external onlyExpectedPool { + _handleIzumiV3SwapCallback(amountY, data); + } + + // ==== Private Functions ==== + /// @dev Common logic for iZiSwap callbacks + /// @param amountToPay The amount of tokens to be sent to the pool + /// @param data The data passed through by the caller + function _handleIzumiV3SwapCallback( + uint256 amountToPay, + bytes calldata data + ) private { + if (amountToPay == 0) { + revert IzumiV3SwapCallbackNotPositiveAmount(); + } + + address tokenIn = abi.decode(data, (address)); + LibAsset.transferERC20(tokenIn, msg.sender, amountToPay); + } +} diff --git a/src/Periphery/LDA/Facets/KatanaV3Facet.sol b/src/Periphery/LDA/Facets/KatanaV3Facet.sol new file mode 100644 index 000000000..0f93d7591 --- /dev/null +++ b/src/Periphery/LDA/Facets/KatanaV3Facet.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { IKatanaV3Pool } from "lifi/Interfaces/KatanaV3/IKatanaV3Pool.sol"; +import { IKatanaV3Governance } from "lifi/Interfaces/KatanaV3/IKatanaV3Governance.sol"; +import { IKatanaV3AggregateRouter } from "lifi/Interfaces/KatanaV3/IKatanaV3AggregateRouter.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; + +/// @title KatanaV3Facet +/// @author LI.FI (https://li.fi) +/// @notice Handles KatanaV3 swaps. Callbacks are on the router contract itself. +/// @custom:version 1.0.0 +contract KatanaV3Facet is BaseRouteConstants { + using LibPackedStream for uint256; + + // ==== Constants ==== + /// @dev KatanaV3 swap command for exact input + bytes internal constant KATANA_V3_SWAP_EXACT_IN = hex"00"; + + // ==== External Functions ==== + /// @notice Performs a swap through KatanaV3 pools + /// @dev This function handles swaps through KatanaV3 pools. + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, direction, destinationAddress] + /// @param from Where to take liquidity for swap + /// @param tokenIn Input token + /// @param amountIn Amount of tokenIn to take for swap + function swapKatanaV3( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; + address destinationAddress = stream.readAddress(); + + if (pool == address(0) || destinationAddress == address(0)) + revert InvalidCallData(); + + // get router address from pool governance + address governance = IKatanaV3Pool(pool).governance(); + address router = IKatanaV3Governance(governance).getRouter(); + + // get pool info for constructing the path + uint24 fee = IKatanaV3Pool(pool).fee(); + + // determine tokenOut based on swap direction + address tokenOut = direction + ? IKatanaV3Pool(pool).token1() + : IKatanaV3Pool(pool).token0(); + + // transfer tokens to the router + if (from == msg.sender) { + LibAsset.transferFromERC20(tokenIn, msg.sender, router, amountIn); + } else if (from == address(this)) { + LibAsset.transferERC20(tokenIn, router, amountIn); + } + + // encode the inputs for V3_SWAP_EXACT_IN + // set payerIsUser to false since we already transferred tokens to the router + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode( + destinationAddress, // destinationAddress + amountIn, // amountIn + 0, // amountOutMin (0, as we handle slippage at higher level) + abi.encodePacked(tokenIn, fee, tokenOut), // construct the path for V3 swap (tokenIn -> tokenOut with fee) + false // payerIsUser (false since tokens are already in router) + ); + + // call the router's execute function + // first parameter for execute is the command for V3_SWAP_EXACT_IN (0x00) + IKatanaV3AggregateRouter(router).execute( + KATANA_V3_SWAP_EXACT_IN, + inputs + ); + + // katanaV3SwapCallback implementation is in the router contract itself + } +} diff --git a/src/Periphery/LDA/Facets/NativeWrapperFacet.sol b/src/Periphery/LDA/Facets/NativeWrapperFacet.sol new file mode 100644 index 000000000..941bea569 --- /dev/null +++ b/src/Periphery/LDA/Facets/NativeWrapperFacet.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { IWETH } from "lifi/Interfaces/IWETH.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; + +/// @title NativeWrapperFacet +/// @author LI.FI (https://li.fi) +/// @notice Handles wrapping/unwrapping of native tokens (ETH <-> WETH) +/// @custom:version 1.0.0 +contract NativeWrapperFacet is BaseRouteConstants { + using LibPackedStream for uint256; + + // ==== External Functions ==== + /// @notice Unwraps WETH to native ETH + /// @dev Handles unwrapping WETH and sending native ETH to recipient + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [destinationAddress] + /// @param from Token source. If from == msg.sender, pull tokens via transferFrom. + /// Otherwise, assume tokens are already held by this contract (e.g., address(this) or FUNDS_IN_RECEIVER). + /// @param tokenIn WETH token address + /// @param amountIn Amount of WETH to unwrap + function unwrapNative( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + address destinationAddress; + assembly { + // swapData layout: [length (32 bytes)][data...] + // We want the first 20 bytes of data, right-shifted to get address + destinationAddress := shr(96, mload(add(swapData, 32))) + } + + if (from == msg.sender) { + LibAsset.transferFromERC20( + tokenIn, + msg.sender, + address(this), + amountIn + ); + } + + IWETH(tokenIn).withdraw(amountIn); + if (destinationAddress != address(this)) { + LibAsset.transferNativeAsset( + payable(destinationAddress), + amountIn + ); + } + } + + /// @notice Wraps native ETH to WETH + /// @dev Handles wrapping native ETH to WETH and sending to recipient + /// @param swapData Encoded swap parameters [wrappedNative, destinationAddress] + /// @param amountIn Amount of native ETH to wrap + function wrapNative( + bytes memory swapData, + address, // from is not used + address, // tokenIn is not used + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address wrappedNative = stream.readAddress(); + address destinationAddress = stream.readAddress(); + + if (wrappedNative == address(0)) { + revert InvalidCallData(); + } + + IWETH(wrappedNative).deposit{ value: amountIn }(); + if (destinationAddress != address(this)) { + LibAsset.transferERC20( + wrappedNative, + destinationAddress, + amountIn + ); + } + } +} diff --git a/src/Periphery/LDA/Facets/SyncSwapV2Facet.sol b/src/Periphery/LDA/Facets/SyncSwapV2Facet.sol new file mode 100644 index 000000000..dff275590 --- /dev/null +++ b/src/Periphery/LDA/Facets/SyncSwapV2Facet.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { ISyncSwapPool } from "lifi/Interfaces/ISyncSwapPool.sol"; +import { ISyncSwapVault } from "lifi/Interfaces/ISyncSwapVault.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; + +/// @title SyncSwapV2Facet +/// @author LI.FI (https://li.fi) +/// @notice Handles SyncSwap V2 pool swaps with vault integration +/// @dev Supports both V1 and V2 SyncSwap pools +/// @custom:version 1.0.0 +contract SyncSwapV2Facet { + using LibPackedStream for uint256; + + /// @notice Executes a swap through a SyncSwap V2 pool + /// @dev Handles both V1 (vault-based) and V2 (direct) pool swaps + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, destinationAddress, withdrawMode, isV1Pool, vault] + /// @param from Token source address - if equals msg.sender or this contract, tokens will be transferred; + /// otherwise assumes tokens are at receiver address + /// @param tokenIn Input token address + /// @param amountIn Amount of input tokens + function swapSyncSwapV2( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + address destinationAddress = stream.readAddress(); + + if (pool == address(0) || destinationAddress == address(0)) + revert InvalidCallData(); + + // withdrawMode meaning for SyncSwap via vault: + // 1: Withdraw raw ETH (native) + // 2: Withdraw WETH (wrapped) + // 0: Let the vault decide (ETH for native, WETH for wrapped) + // For ERC-20 tokens the vault just withdraws the ERC-20 + // and this mode byte is read and ignored by the ERC-20 branch. + uint8 withdrawMode = stream.readUint8(); + + if (withdrawMode > 2) revert InvalidCallData(); + + bool isV1Pool = stream.readUint8() == 1; + + address target = isV1Pool ? stream.readAddress() : pool; // target is the vault for V1 pools, the pool for V2 pools + if (isV1Pool && target == address(0)) revert InvalidCallData(); + + if (from == msg.sender) { + LibAsset.transferFromERC20(tokenIn, msg.sender, target, amountIn); + } else if (from == address(this)) { + LibAsset.transferERC20(tokenIn, target, amountIn); + } + // if from is not msg.sender or address(this), it must be FUNDS_IN_RECEIVER + // which means tokens are already in the vault/pool, no transfer needed + + // SyncSwap V1 pools require tokens to be deposited into their vault first + // before they can be used for swapping + if (isV1Pool) { + ISyncSwapVault(target).deposit(tokenIn, pool); + } + + ISyncSwapPool(pool).swap( + abi.encode(tokenIn, destinationAddress, withdrawMode), + from, + address(0), + new bytes(0) + ); + } +} diff --git a/src/Periphery/LDA/Facets/UniV2StyleFacet.sol b/src/Periphery/LDA/Facets/UniV2StyleFacet.sol new file mode 100644 index 000000000..7ff162e7d --- /dev/null +++ b/src/Periphery/LDA/Facets/UniV2StyleFacet.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { IUniV2StylePool } from "lifi/Interfaces/IUniV2StylePool.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; +import { WrongPoolReserves } from "../LiFiDEXAggregatorErrors.sol"; + +/// @title UniV2StyleFacet +/// @author LI.FI (https://li.fi) +/// @notice Handles UniswapV2-style swaps (UniV2, SushiSwap, PancakeV2, etc.) +/// @custom:version 1.0.0 +contract UniV2StyleFacet is BaseRouteConstants { + using LibPackedStream for uint256; + + // ==== Constants ==== + /// @dev Fee denominator for UniV2-style pools (100% = 1_000_000) + uint256 private constant FEE_DENOMINATOR = 1_000_000; + + // ==== External Functions ==== + /// @notice Executes a UniswapV2-style swap + /// @dev Handles token transfers and calculates output amounts based on pool reserves + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, direction, destinationAddress, fee] + /// @param from Token source address - if equals msg.sender or this contract, tokens will be transferred; + /// otherwise assumes tokens are at receiver address (FUNDS_IN_RECEIVER) + /// @param tokenIn Input token address + /// @param amountIn Amount of input tokens + function swapUniV2( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; + address destinationAddress = stream.readAddress(); + uint24 fee = stream.readUint24(); // pool fee in 1/1_000_000 + + if ( + pool == address(0) || + destinationAddress == address(0) || + fee >= FEE_DENOMINATOR + ) { + revert InvalidCallData(); + } + + // Transfer tokens to pool if needed + if (from == address(this)) { + LibAsset.transferERC20(tokenIn, pool, amountIn); + } else if (from == msg.sender) { + LibAsset.transferFromERC20(tokenIn, msg.sender, pool, amountIn); + } + + // Get reserves and calculate output + (uint256 r0, uint256 r1, ) = IUniV2StylePool(pool).getReserves(); + if (r0 == 0 || r1 == 0) revert WrongPoolReserves(); + + (uint256 reserveIn, uint256 reserveOut) = direction + ? (r0, r1) + : (r1, r0); + + // Calculate actual input amount from pool balance + amountIn = IERC20(tokenIn).balanceOf(pool) - reserveIn; + + uint256 amountInWithFee = amountIn * (FEE_DENOMINATOR - fee); + uint256 amountOut = (amountInWithFee * reserveOut) / + (reserveIn * FEE_DENOMINATOR + amountInWithFee); + + (uint256 amount0Out, uint256 amount1Out) = direction + ? (uint256(0), amountOut) + : (amountOut, uint256(0)); + + IUniV2StylePool(pool).swap( + amount0Out, + amount1Out, + destinationAddress, + new bytes(0) + ); + } +} diff --git a/src/Periphery/LDA/Facets/UniV3StyleFacet.sol b/src/Periphery/LDA/Facets/UniV3StyleFacet.sol new file mode 100644 index 000000000..24a2724b9 --- /dev/null +++ b/src/Periphery/LDA/Facets/UniV3StyleFacet.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { LibUniV3Logic } from "lifi/Libraries/LibUniV3Logic.sol"; +import { LibCallbackAuthenticator } from "lifi/Libraries/LibCallbackAuthenticator.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { IUniV3StylePool } from "lifi/Interfaces/IUniV3StylePool.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { SwapCallbackNotExecuted } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; +import { PoolCallbackAuthenticator } from "lifi/Periphery/LDA/PoolCallbackAuthenticator.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; + +/// @title UniV3StyleFacet +/// @author LI.FI (https://li.fi) +/// @notice Handles Uniswap V3 style swaps with callback verification +/// @custom:version 1.0.0 +contract UniV3StyleFacet is BaseRouteConstants, PoolCallbackAuthenticator { + using LibCallbackAuthenticator for *; + using LibPackedStream for uint256; + + // ==== Constants ==== + /// @dev Minimum sqrt price ratio for UniV3 pool swaps + uint160 internal constant MIN_SQRT_RATIO = 4295128739; + /// @dev Maximum sqrt price ratio for UniV3 pool swaps + uint160 internal constant MAX_SQRT_RATIO = + 1461446703485210103287273052203988822378723970342; + + // ==== External Functions ==== + /// @notice Executes a swap through a UniV3-style pool + /// @dev Handles token transfers and manages callback verification + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, direction, destinationAddress] + /// @param from Token source address - if equals msg.sender, tokens will be pulled from the caller + /// @param tokenIn Input token address + /// @param amountIn Amount of input tokens + function swapUniV3( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; + address destinationAddress = stream.readAddress(); + + if ( + pool == address(0) || + destinationAddress == address(0) || + amountIn > uint256(type(int256).max) + ) { + revert InvalidCallData(); + } + + // Transfer tokens if needed + if (from == msg.sender) { + LibAsset.transferFromERC20( + tokenIn, + msg.sender, + address(this), + amountIn + ); + } + + // Arm callback protection + LibCallbackAuthenticator.arm(pool); + + // Execute swap + IUniV3StylePool(pool).swap( + destinationAddress, + direction, + int256(amountIn), + direction ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, + abi.encode(tokenIn) + ); + + // Verify callback was called (arm should be cleared by callback) + if ( + LibCallbackAuthenticator.callbackStorage().expected != address(0) + ) { + revert SwapCallbackNotExecuted(); + } + } + + // ==== Callback Functions ==== + /// @notice Callback for Uniswap V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for PancakeSwap V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function pancakeV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for Ramses V2 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function ramsesV2SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for Xei V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function xeiV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for DragonSwap V2 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function dragonswapV2SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for Agni swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function agniSwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for FusionX V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function fusionXV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for VVS V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function vvsV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for Sup V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function supV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for Zebra V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function zebraV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for HyperSwap V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function hyperswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for Laminar V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function laminarV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for XSwap swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function xswapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for RabbitSwap V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function rabbitSwapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } + + /// @notice Callback for EnosysDEX V3 swaps + /// @dev Verifies callback source and handles token transfer + /// @param amount0Delta The amount of token0 being borrowed/repaid + /// @param amount1Delta The amount of token1 being borrowed/repaid + /// @param data Encoded data containing input token address + function enosysdexV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external onlyExpectedPool { + LibUniV3Logic.handleCallback(amount0Delta, amount1Delta, data); + } +} diff --git a/src/Periphery/LDA/Facets/VelodromeV2Facet.sol b/src/Periphery/LDA/Facets/VelodromeV2Facet.sol new file mode 100644 index 000000000..ff52a8df8 --- /dev/null +++ b/src/Periphery/LDA/Facets/VelodromeV2Facet.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LibPackedStream } from "lifi/Libraries/LibPackedStream.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { IVelodromeV2Pool } from "lifi/Interfaces/IVelodromeV2Pool.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseRouteConstants } from "../BaseRouteConstants.sol"; +import { WrongPoolReserves } from "../LiFiDEXAggregatorErrors.sol"; + +/// @title VelodromeV2Facet +/// @author LI.FI (https://li.fi) +/// @notice Handles Velodrome V2 pool swaps +/// @custom:version 1.0.0 +contract VelodromeV2Facet is BaseRouteConstants { + using LibPackedStream for uint256; + + // ==== Constants ==== + /// @dev Flag to enable post-swap callback with flashloan data + uint8 internal constant CALLBACK_ENABLED = 1; + + // ==== External Functions ==== + /// @notice Performs a swap through VelodromeV2 pools + /// @dev Handles token transfers and optional callbacks, with comprehensive safety checks + /// This function is marked `payable` to support multihop routes that start + /// with a native asset (e.g., ETH -> WETH -> USDC). The initial `msg.value` + /// is preserved through the `delegatecall` chain and this function must be + /// able to receive it, even if the value is not used in this step. + /// @param swapData Encoded swap parameters [pool, direction, destinationAddress, callback] + /// @param from Token source address - if equals msg.sender or this contract, tokens will be transferred; + /// otherwise assumes tokens are at receiver address (FUNDS_IN_RECEIVER) + /// @param tokenIn Input token address + /// @param amountIn Amount of input tokens + function swapVelodromeV2( + bytes memory swapData, + address from, + address tokenIn, + uint256 amountIn + ) external payable { + uint256 stream = LibPackedStream.createStream(swapData); + + address pool = stream.readAddress(); + bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; + address destinationAddress = stream.readAddress(); + + if (pool == address(0) || destinationAddress == address(0)) + revert InvalidCallData(); + + bool callback = stream.readUint8() == CALLBACK_ENABLED; // if true then run callback after swap with tokenIn as flashloan data. + // Will revert if contract (destinationAddress) does not implement IVelodromeV2PoolCallee. + + if (from == FUNDS_IN_RECEIVER) { + (uint256 reserve0, uint256 reserve1, ) = IVelodromeV2Pool(pool) + .getReserves(); + if (reserve0 == 0 || reserve1 == 0) revert WrongPoolReserves(); + uint256 reserveIn = direction ? reserve0 : reserve1; + + amountIn = IERC20(tokenIn).balanceOf(pool) - reserveIn; + } else { + if (from == address(this)) { + LibAsset.transferERC20(tokenIn, pool, amountIn); + } else if (from == msg.sender) { + LibAsset.transferFromERC20( + tokenIn, + msg.sender, + pool, + amountIn + ); + } + } + + // calculate the expected output amount using the pool's getAmountOut function + uint256 amountOut = IVelodromeV2Pool(pool).getAmountOut( + amountIn, + tokenIn + ); + + // set the appropriate output amount based on which token is being swapped + // determine output amounts based on direction + uint256 amount0Out = direction ? 0 : amountOut; + uint256 amount1Out = direction ? amountOut : 0; + + // 'swap' function from IVelodromeV2Pool should be called from a contract which performs important safety checks. + // Safety Checks Covered: + // - Reentrancy: LiFiDEXAggregator has a custom lock() modifier + // - Token transfer safety: SafeERC20 is used to ensure token transfers revert on failure + // - Expected output verification: The contract calls getAmountOut (including fees) before executing the swap + // - Flashloan trigger: A flashloan flag is used to determine if the callback should be triggered + // - Post-swap verification: In processRouteInternal, it verifies that the destinationAddress receives at least minAmountOut + // and that the sender's final balance is not less than the initial balance + // - Immutable interaction: Velodrome V2 pools and the router are not upgradable, + // so we can rely on the behavior of getAmountOut and swap + + // ATTENTION FOR CALLBACKS / HOOKS: + // - destinationAddress contracts should validate that msg.sender is the Velodrome pool contract who is calling the hook + // - destinationAddress contracts must not manipulate their own tokenOut balance + // (as this may bypass/invalidate the built-in slippage protection) + // - @developers: never trust balance-based slippage protection for callback of destinationAddress + // - @integrators: do not use slippage guarantees when destinationAddress is a contract with side-effects + IVelodromeV2Pool(pool).swap( + amount0Out, + amount1Out, + destinationAddress, + callback ? abi.encode(tokenIn) : bytes("") + ); + } +} diff --git a/src/Periphery/LDA/LiFiDEXAggregatorDiamond.sol b/src/Periphery/LDA/LiFiDEXAggregatorDiamond.sol new file mode 100644 index 000000000..bed1beb80 --- /dev/null +++ b/src/Periphery/LDA/LiFiDEXAggregatorDiamond.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LiFiDiamond } from "../../LiFiDiamond.sol"; + +/// @title LiFiDEXAggregatorDiamond +/// @author LI.FI (https://li.fi) +/// @notice Base EIP-2535 Diamond Proxy Contract for LDA (LiFi DEX Aggregator). +/// @custom:version 1.0.0 +contract LiFiDEXAggregatorDiamond is LiFiDiamond { + constructor( + address _contractOwner, + address _diamondCutFacet + ) LiFiDiamond(_contractOwner, _diamondCutFacet) {} +} diff --git a/src/Periphery/LDA/LiFiDEXAggregatorErrors.sol b/src/Periphery/LDA/LiFiDEXAggregatorErrors.sol new file mode 100644 index 000000000..09c66fe7d --- /dev/null +++ b/src/Periphery/LDA/LiFiDEXAggregatorErrors.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +/// @custom:version 1.0.0 +pragma solidity ^0.8.17; + +error SwapCallbackNotExecuted(); +error WrongPoolReserves(); diff --git a/src/Periphery/LDA/PoolCallbackAuthenticator.sol b/src/Periphery/LDA/PoolCallbackAuthenticator.sol new file mode 100644 index 000000000..49c11f8d5 --- /dev/null +++ b/src/Periphery/LDA/PoolCallbackAuthenticator.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibCallbackAuthenticator } from "lifi/Libraries/LibCallbackAuthenticator.sol"; + +/// @title PoolCallbackAuthenticator +/// @author LI.FI (https://li.fi) +/// @notice Abstract contract providing pool callback authentication functionality +/// @custom:version 1.0.0 +abstract contract PoolCallbackAuthenticator { + using LibCallbackAuthenticator for *; + + /// @dev Ensures callback is from expected pool and cleans up after callback + modifier onlyExpectedPool() { + LibCallbackAuthenticator.verifyCallbackSender(); + _; + LibCallbackAuthenticator.disarm(); + } +} diff --git a/src/Periphery/LiFiDEXAggregator.sol b/src/Periphery/LiFiDEXAggregator.sol deleted file mode 100644 index 0c4b99555..000000000 --- a/src/Periphery/LiFiDEXAggregator.sol +++ /dev/null @@ -1,1820 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -pragma solidity ^0.8.17; - -import { SafeERC20, IERC20, IERC20Permit } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; -import { WithdrawablePeriphery } from "lifi/Helpers/WithdrawablePeriphery.sol"; -import { IVelodromeV2Pool } from "lifi/Interfaces/IVelodromeV2Pool.sol"; -import { IAlgebraPool } from "lifi/Interfaces/IAlgebraPool.sol"; -import { IiZiSwapPool } from "lifi/Interfaces/IiZiSwapPool.sol"; -import { ISyncSwapVault } from "lifi/Interfaces/ISyncSwapVault.sol"; -import { ISyncSwapPool } from "lifi/Interfaces/ISyncSwapPool.sol"; -import { IKatanaV3Pool } from "lifi/Interfaces/KatanaV3/IKatanaV3Pool.sol"; -import { IKatanaV3Governance } from "lifi/Interfaces/KatanaV3/IKatanaV3Governance.sol"; -import { IKatanaV3AggregateRouter } from "lifi/Interfaces/KatanaV3/IKatanaV3AggregateRouter.sol"; -import { InvalidConfig, InvalidCallData } from "lifi/Errors/GenericErrors.sol"; -import { LibAsset } from "lifi/Libraries/LibAsset.sol"; - -address constant NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; -address constant IMPOSSIBLE_POOL_ADDRESS = 0x0000000000000000000000000000000000000001; -address constant INTERNAL_INPUT_SOURCE = 0x0000000000000000000000000000000000000000; - -uint8 constant LOCKED = 2; -uint8 constant NOT_LOCKED = 1; -uint8 constant PAUSED = 2; -uint8 constant NOT_PAUSED = 1; - -/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) -uint160 constant MIN_SQRT_RATIO = 4295128739; -/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) -uint160 constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; - -/// @dev iZiSwap pool price points boundaries -int24 constant IZUMI_LEFT_MOST_PT = -800000; -int24 constant IZUMI_RIGHT_MOST_PT = 800000; - -uint8 constant DIRECTION_TOKEN0_TO_TOKEN1 = 1; -uint8 constant CALLBACK_ENABLED = 1; - -/// @dev Pool type identifiers used to determine which DEX protocol to interact with during swaps -uint8 constant POOL_TYPE_UNIV2 = 0; -uint8 constant POOL_TYPE_UNIV3 = 1; -uint8 constant POOL_TYPE_WRAP_NATIVE = 2; -uint8 constant POOL_TYPE_BENTO_BRIDGE = 3; -uint8 constant POOL_TYPE_TRIDENT = 4; -uint8 constant POOL_TYPE_CURVE = 5; -uint8 constant POOL_TYPE_VELODROME_V2 = 6; -uint8 constant POOL_TYPE_ALGEBRA = 7; -uint8 constant POOL_TYPE_IZUMI_V3 = 8; -uint8 constant POOL_TYPE_SYNCSWAP = 9; -uint8 constant POOL_TYPE_KATANA_V3 = 10; - -/// @dev command for V3_SWAP_EXACT_IN (0x00) for KatanaV3 -bytes constant KATANA_V3_SWAP_EXACT_IN = hex"00"; - -/// @title LiFiDEXAggregator -/// @author Ilya Lyalin (contract copied from: -/// https://github.com/sushiswap/sushiswap/blob/c8c80dec821003eb72eb77c7e0446ddde8ca9e1e/ -/// protocols/route-processor/contracts/RouteProcessor4.sol) -/// @notice Processes calldata to swap using various DEXs -/// @custom:version 1.12.0 -contract LiFiDEXAggregator is WithdrawablePeriphery { - using SafeERC20 for IERC20; - using Approve for IERC20; - using SafeERC20 for IERC20Permit; - using InputStream for uint256; - - event Route( - address indexed from, - address to, - address indexed tokenIn, - address indexed tokenOut, - uint256 amountIn, - uint256 amountOutMin, - uint256 amountOut - ); - - error MinimalOutputBalanceViolation(uint256 amountOut); - error RouteProcessorLocked(); - error RouteProcessorPaused(); - error CallerNotOwnerOrPriviledged(); - error UnknownCommandCode(); - error UnknownPoolType(); - error MinimalInputBalanceViolation(uint256 available, uint256 required); - error UniswapV3SwapUnexpected(); - error UniswapV3SwapCallbackUnknownSource(); - error UniswapV3SwapCallbackNotPositiveAmount(); - error WrongPoolReserves(); - error AlgebraSwapUnexpected(); - error IzumiV3SwapUnexpected(); - error IzumiV3SwapCallbackUnknownSource(); - error IzumiV3SwapCallbackNotPositiveAmount(); - - IBentoBoxMinimal public immutable BENTO_BOX; - mapping(address => bool) public priviledgedUsers; - address private lastCalledPool; - - uint8 private unlocked = NOT_LOCKED; - uint8 private paused = NOT_PAUSED; - modifier lock() { - if (unlocked != NOT_LOCKED) revert RouteProcessorLocked(); - if (paused != NOT_PAUSED) revert RouteProcessorPaused(); - unlocked = LOCKED; - _; - unlocked = NOT_LOCKED; - } - - modifier onlyOwnerOrPriviledgedUser() { - if (!(msg.sender == owner || priviledgedUsers[msg.sender])) - revert CallerNotOwnerOrPriviledged(); - _; - } - - constructor( - address _bentoBox, - address[] memory priviledgedUserList, - address _owner - ) WithdrawablePeriphery(_owner) { - if (_owner == address(0)) { - revert InvalidConfig(); - } - BENTO_BOX = IBentoBoxMinimal(_bentoBox); - lastCalledPool = IMPOSSIBLE_POOL_ADDRESS; - - for (uint256 i = 0; i < priviledgedUserList.length; i++) { - priviledgedUsers[priviledgedUserList[i]] = true; - } - } - - function setPriviledge(address user, bool priviledge) external onlyOwner { - priviledgedUsers[user] = priviledge; - } - - function pause() external onlyOwnerOrPriviledgedUser { - paused = PAUSED; - } - - function resume() external onlyOwnerOrPriviledgedUser { - paused = NOT_PAUSED; - } - - /// @notice For native unwrapping - receive() external payable {} - - /// @notice Processes the route generated off-chain. Has a lock - /// @param tokenIn Address of the input token - /// @param amountIn Amount of the input token - /// @param tokenOut Address of the output token - /// @param amountOutMin Minimum amount of the output token - /// @return amountOut Actual amount of the output token - function processRoute( - address tokenIn, - uint256 amountIn, - address tokenOut, - uint256 amountOutMin, - address to, - bytes memory route - ) external payable lock returns (uint256 amountOut) { - return - processRouteInternal( - tokenIn, - amountIn, - tokenOut, - amountOutMin, - to, - route - ); - } - - /// @notice Transfers some value to and then processes the route - /// @param transferValueTo Address where the value should be transferred - /// @param amountValueTransfer How much value to transfer - /// @param tokenIn Address of the input token - /// @param amountIn Amount of the input token - /// @param tokenOut Address of the output token - /// @param amountOutMin Minimum amount of the output token - /// @return amountOut Actual amount of the output token - function transferValueAndprocessRoute( - address payable transferValueTo, - uint256 amountValueTransfer, - address tokenIn, - uint256 amountIn, - address tokenOut, - uint256 amountOutMin, - address to, - bytes memory route - ) external payable lock returns (uint256 amountOut) { - SafeTransferLib.safeTransferETH(transferValueTo, amountValueTransfer); - return - processRouteInternal( - tokenIn, - amountIn, - tokenOut, - amountOutMin, - to, - route - ); - } - - /// @notice Processes the route generated off-chain - /// @param tokenIn Address of the input token - /// @param amountIn Amount of the input token - /// @param tokenOut Address of the output token - /// @param amountOutMin Minimum amount of the output token - /// @return amountOut Actual amount of the output token - function processRouteInternal( - address tokenIn, - uint256 amountIn, - address tokenOut, - uint256 amountOutMin, - address to, - bytes memory route - ) private returns (uint256 amountOut) { - uint256 balanceInInitial = tokenIn == NATIVE_ADDRESS - ? 0 - : IERC20(tokenIn).balanceOf(msg.sender); - uint256 balanceOutInitial = tokenOut == NATIVE_ADDRESS - ? address(to).balance - : IERC20(tokenOut).balanceOf(to); - - uint256 realAmountIn = amountIn; - { - uint256 step = 0; - uint256 stream = InputStream.createStream(route); - while (stream.isNotEmpty()) { - uint8 commandCode = stream.readUint8(); - if (commandCode == 1) { - uint256 usedAmount = processMyERC20(stream); - if (step == 0) realAmountIn = usedAmount; - } else if (commandCode == 2) - processUserERC20(stream, amountIn); - else if (commandCode == 3) { - uint256 usedAmount = processNative(stream); - if (step == 0) realAmountIn = usedAmount; - } else if (commandCode == 4) processOnePool(stream); - else if (commandCode == 5) processInsideBento(stream); - else if (commandCode == 6) applyPermit(tokenIn, stream); - else revert UnknownCommandCode(); - ++step; - } - } - - uint256 balanceInFinal = tokenIn == NATIVE_ADDRESS - ? 0 - : IERC20(tokenIn).balanceOf(msg.sender); - if (balanceInFinal + amountIn < balanceInInitial) - revert MinimalInputBalanceViolation( - balanceInFinal + amountIn, - balanceInInitial - ); - - uint256 balanceOutFinal = tokenOut == NATIVE_ADDRESS - ? address(to).balance - : IERC20(tokenOut).balanceOf(to); - if (balanceOutFinal < balanceOutInitial + amountOutMin) - revert MinimalOutputBalanceViolation( - balanceOutFinal - balanceOutInitial - ); - - amountOut = balanceOutFinal - balanceOutInitial; - - emit Route( - msg.sender, - to, - tokenIn, - tokenOut, - realAmountIn, - amountOutMin, - amountOut - ); - } - - /// @notice Applies ERC-2612 permit - /// @param tokenIn permitted token - /// @param stream Streamed program - function applyPermit(address tokenIn, uint256 stream) private { - uint256 value = stream.readUint(); - uint256 deadline = stream.readUint(); - uint8 v = stream.readUint8(); - bytes32 r = stream.readBytes32(); - bytes32 s = stream.readBytes32(); - IERC20Permit(tokenIn).safePermit( - msg.sender, - address(this), - value, - deadline, - v, - r, - s - ); - } - - /// @notice Processes native coin: call swap for all pools that swap from native coin - /// @param stream Streamed program - function processNative( - uint256 stream - ) private returns (uint256 amountTotal) { - amountTotal = address(this).balance; - distributeAndSwap(stream, address(this), NATIVE_ADDRESS, amountTotal); - } - - /// @notice Processes ERC20 token from this contract balance: - /// @notice Call swap for all pools that swap from this token - /// @param stream Streamed program - function processMyERC20( - uint256 stream - ) private returns (uint256 amountTotal) { - address token = stream.readAddress(); - amountTotal = IERC20(token).balanceOf(address(this)); - unchecked { - if (amountTotal > 0) amountTotal -= 1; // slot undrain protection - } - distributeAndSwap(stream, address(this), token, amountTotal); - } - - /// @notice Processes ERC20 token from msg.sender balance: - /// @notice Call swap for all pools that swap from this token - /// @param stream Streamed program - /// @param amountTotal Amount of tokens to take from msg.sender - function processUserERC20(uint256 stream, uint256 amountTotal) private { - address token = stream.readAddress(); - distributeAndSwap(stream, msg.sender, token, amountTotal); - } - - /// @notice Processes ERC20 token for cases when the token has only one output pool - /// @notice In this case liquidity is already at pool balance. This is an optimization - /// @notice Call swap for all pools that swap from this token - /// @dev WARNING: This function passes amountIn as 0 which may not work with some UniswapV3 - /// @dev forks that require non-zero amounts for their pricing/slippage calculations. - /// @dev Use with caution for V3-style pools. - /// @param stream Streamed program - function processOnePool(uint256 stream) private { - address token = stream.readAddress(); - swap(stream, INTERNAL_INPUT_SOURCE, token, 0); - } - - /// @notice Processes Bento tokens - /// @notice Call swap for all pools that swap from this token - /// @param stream Streamed program - function processInsideBento(uint256 stream) private { - address token = stream.readAddress(); - uint256 amountTotal = BENTO_BOX.balanceOf(token, address(this)); - unchecked { - if (amountTotal > 0) amountTotal -= 1; // slot undrain protection - } - distributeAndSwap(stream, address(this), token, amountTotal); - } - - /// @notice Distributes amountTotal to several pools according to their shares and calls swap for each pool - /// @param stream Streamed program - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountTotal Total amount of tokenIn for swaps - function distributeAndSwap( - uint256 stream, - address from, - address tokenIn, - uint256 amountTotal - ) private { - uint8 num = stream.readUint8(); - unchecked { - for (uint256 i = 0; i < num; ++i) { - uint16 share = stream.readUint16(); - uint256 amount = (amountTotal * share) / - type(uint16).max /*65535*/; - amountTotal -= amount; - swap(stream, from, tokenIn, amount); - } - } - } - - /// @notice Makes swap - /// @param stream Streamed program - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swap( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - uint8 poolType = stream.readUint8(); - if (poolType == POOL_TYPE_UNIV2) - swapUniV2(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_UNIV3) - swapUniV3(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_WRAP_NATIVE) - wrapNative(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_BENTO_BRIDGE) - bentoBridge(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_TRIDENT) - swapTrident(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_CURVE) - swapCurve(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_VELODROME_V2) - swapVelodromeV2(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_ALGEBRA) - swapAlgebra(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_IZUMI_V3) - swapIzumiV3(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_SYNCSWAP) - swapSyncSwap(stream, from, tokenIn, amountIn); - else if (poolType == POOL_TYPE_KATANA_V3) - swapKatanaV3(stream, from, tokenIn, amountIn); - else revert UnknownPoolType(); - } - - /// @notice Wraps/unwraps native token - /// @param stream [direction & fake, recipient, wrapToken?] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function wrapNative( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - uint8 directionAndFake = stream.readUint8(); - address to = stream.readAddress(); - - if (directionAndFake & 1 == 1) { - // wrap native - address wrapToken = stream.readAddress(); - if (directionAndFake & 2 == 0) - IWETH(wrapToken).deposit{ value: amountIn }(); - if (to != address(this)) - IERC20(wrapToken).safeTransfer(to, amountIn); - } else { - // unwrap native - if (directionAndFake & 2 == 0) { - if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom( - msg.sender, - address(this), - amountIn - ); - IWETH(tokenIn).withdraw(amountIn); - } - SafeTransferLib.safeTransferETH(to, amountIn); - } - } - - /// @notice Bridge/unbridge tokens to/from Bento - /// @param stream [direction, recipient] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function bentoBridge( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - uint8 direction = stream.readUint8(); - address to = stream.readAddress(); - - if (direction > 0) { - // outside to Bento - // deposit to arbitrary recipient is possible only from address(BENTO_BOX) - if (from == address(this)) - IERC20(tokenIn).safeTransfer(address(BENTO_BOX), amountIn); - else if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom( - msg.sender, - address(BENTO_BOX), - amountIn - ); - else { - // tokens already are at address(BENTO_BOX) - amountIn = - IERC20(tokenIn).balanceOf(address(BENTO_BOX)) + - BENTO_BOX.strategyData(tokenIn).balance - - BENTO_BOX.totals(tokenIn).elastic; - } - BENTO_BOX.deposit(tokenIn, address(BENTO_BOX), to, amountIn, 0); - } else { - // Bento to outside - if (from != INTERNAL_INPUT_SOURCE) { - BENTO_BOX.transfer(tokenIn, from, address(this), amountIn); - } else amountIn = BENTO_BOX.balanceOf(tokenIn, address(this)); - BENTO_BOX.withdraw(tokenIn, address(this), to, 0, amountIn); - } - } - - /// @notice UniswapV2 pool swap - /// @param stream [pool, direction, recipient, fee] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapUniV2( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - uint8 direction = stream.readUint8(); - address to = stream.readAddress(); - uint24 fee = stream.readUint24(); // pool fee in 1/1_000_000 - - if (from == address(this)) - IERC20(tokenIn).safeTransfer(pool, amountIn); - else if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom(msg.sender, pool, amountIn); - - (uint256 r0, uint256 r1, ) = IUniswapV2Pair(pool).getReserves(); - if (r0 == 0 || r1 == 0) revert WrongPoolReserves(); - (uint256 reserveIn, uint256 reserveOut) = direction == - DIRECTION_TOKEN0_TO_TOKEN1 - ? (r0, r1) - : (r1, r0); - amountIn = IERC20(tokenIn).balanceOf(pool) - reserveIn; // tokens already were transferred - - uint256 amountInWithFee = amountIn * (1_000_000 - fee); - uint256 amountOut = (amountInWithFee * reserveOut) / - (reserveIn * 1_000_000 + amountInWithFee); - (uint256 amount0Out, uint256 amount1Out) = direction == - DIRECTION_TOKEN0_TO_TOKEN1 - ? (uint256(0), amountOut) - : (amountOut, uint256(0)); - IUniswapV2Pair(pool).swap(amount0Out, amount1Out, to, new bytes(0)); - } - - /// @notice Trident pool swap - /// @param stream [pool, swapData] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapTrident( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - bytes memory swapData = stream.readBytes(); - - if (from != INTERNAL_INPUT_SOURCE) { - BENTO_BOX.transfer(tokenIn, from, pool, amountIn); - } - - IPool(pool).swap(swapData); - } - - /// @notice UniswapV3 pool swap - /// @param stream [pool, direction, recipient] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapUniV3( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - bool direction = stream.readUint8() > 0; - address recipient = stream.readAddress(); - - if ( - pool == address(0) || - pool == IMPOSSIBLE_POOL_ADDRESS || - recipient == address(0) - ) revert InvalidCallData(); - - if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom( - msg.sender, - address(this), - uint256(amountIn) - ); - - lastCalledPool = pool; - IUniswapV3Pool(pool).swap( - recipient, - direction, - int256(amountIn), - direction ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, - abi.encode(tokenIn) - ); - if (lastCalledPool != IMPOSSIBLE_POOL_ADDRESS) - revert UniswapV3SwapUnexpected(); // Just to be sure - } - - /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory. - /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call - function uniswapV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) public { - if (msg.sender != lastCalledPool) - revert UniswapV3SwapCallbackUnknownSource(); - int256 amount = amount0Delta > 0 ? amount0Delta : amount1Delta; - if (amount <= 0) revert UniswapV3SwapCallbackNotPositiveAmount(); - - lastCalledPool = IMPOSSIBLE_POOL_ADDRESS; - address tokenIn = abi.decode(data, (address)); - IERC20(tokenIn).safeTransfer(msg.sender, uint256(amount)); - } - - /// @notice Called to `msg.sender` after executing a swap via IAlgebraPool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method _must_ be checked to be a AlgebraPool deployed by the canonical AlgebraFactory. - /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the IAlgebraPoolActions#swap call - function algebraSwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via PancakeV3Pool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the PancakeV3Pool#swap call - function pancakeV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via RaExchangeV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the RaExchangeV3#swap call - function ramsesV2SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via XeiV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the XeiV3#swap call - function xeiV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via DragonSwapV2#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the DragonSwapV2#swap call - function dragonswapV2SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via AgniV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) - /// by the pool by the end of the swap. If positive, the callback must send that amount of - /// token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) - /// by the pool by the end of the swap. If positive, the callback must send that amount of - /// token1 to the pool. - /// @param data Any data passed through by the caller via the AgniV3#swap call - function agniSwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via FusionXV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the FusionXV3#swap call - function fusionXV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via VVS FinanceV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the VVS Finance V3#swap call - function vvsV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via SupSwapV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the SupSwapV3#swap call - function supV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via ZebraV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the ZebraV3#swap call - function zebraV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Performs a swap through iZiSwap V3 pools - /// @dev This function handles both X to Y and Y to X swaps through iZiSwap V3 pools - /// @param stream [pool, direction, recipient] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapIzumiV3( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - uint8 direction = stream.readUint8(); // 0 = Y2X, 1 = X2Y - address recipient = stream.readAddress(); - if ( - pool == address(0) || - pool == IMPOSSIBLE_POOL_ADDRESS || - recipient == address(0) || - amountIn > type(uint128).max - ) revert InvalidCallData(); - - if (from == msg.sender) { - IERC20(tokenIn).safeTransferFrom( - msg.sender, - address(this), - amountIn - ); - } - - lastCalledPool = pool; - - if (direction == DIRECTION_TOKEN0_TO_TOKEN1) { - IiZiSwapPool(pool).swapX2Y( - recipient, - uint128(amountIn), - IZUMI_LEFT_MOST_PT + 1, - abi.encode(tokenIn) - ); - } else { - IiZiSwapPool(pool).swapY2X( - recipient, - uint128(amountIn), - IZUMI_RIGHT_MOST_PT - 1, - abi.encode(tokenIn) - ); - } - - // After the swapX2Y or swapY2X call, the callback should reset lastCalledPool to IMPOSSIBLE_POOL_ADDRESS. - // If it hasn't, it means the callback either didn't happen, was incorrect, or the pool misbehaved so we revert - // to protect against misuse or faulty integrations - if (lastCalledPool != IMPOSSIBLE_POOL_ADDRESS) { - revert IzumiV3SwapUnexpected(); - } - } - - /// @notice Performs a swap through SyncSwap pools - /// @dev This function handles both X to Y and Y to X swaps through SyncSwap pools. - /// See [SyncSwap API documentation](https://docs.syncswap.xyz/api-documentation) for protocol details. - /// @param stream [pool, to, withdrawMode, isV1Pool, vault] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapSyncSwap( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - address to = stream.readAddress(); - - if (pool == address(0) || to == address(0)) revert InvalidCallData(); - - // withdrawMode meaning for SyncSwap via vault: - // 1: Withdraw raw ETH (native) - // 2: Withdraw WETH (wrapped) - // 0: Let the vault decide (ETH for native, WETH for wrapped) - // For ERC-20 tokens the vault just withdraws the ERC-20 - // and this mode byte is read and ignored by the ERC-20 branch. - uint8 withdrawMode = stream.readUint8(); - - if (withdrawMode > 2) revert InvalidCallData(); - - bool isV1Pool = stream.readUint8() == 1; - - address target = isV1Pool ? stream.readAddress() : pool; // target is the vault for V1 pools, the pool for V2 pools - if (isV1Pool && target == address(0)) revert InvalidCallData(); - - if (from == msg.sender) { - IERC20(tokenIn).safeTransferFrom(msg.sender, target, amountIn); - } else if (from == address(this)) { - IERC20(tokenIn).safeTransfer(target, amountIn); - } - // if from is not msg.sender or address(this), it must be INTERNAL_INPUT_SOURCE - // which means tokens are already in the vault/pool, no transfer needed - - if (isV1Pool) { - ISyncSwapVault(target).deposit(tokenIn, pool); - } - - bytes memory data = abi.encode(tokenIn, to, withdrawMode); - - ISyncSwapPool(pool).swap(data, from, address(0), new bytes(0)); - } - - /// @notice Performs a swap through KatanaV3 pools - /// @dev This function handles swaps through KatanaV3 pools. - /// @param stream [pool, direction, recipient] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapKatanaV3( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; - address recipient = stream.readAddress(); - - if ( - pool == address(0) || - pool == IMPOSSIBLE_POOL_ADDRESS || - recipient == address(0) - ) revert InvalidCallData(); - - // get router address from pool governance - address governance = IKatanaV3Pool(pool).governance(); - address router = IKatanaV3Governance(governance).getRouter(); - - // get pool info for constructing the path - uint24 fee = IKatanaV3Pool(pool).fee(); - - // determine tokenOut based on swap direction - address tokenOut = direction - ? IKatanaV3Pool(pool).token1() - : IKatanaV3Pool(pool).token0(); - - // transfer tokens to the router - if (from == msg.sender) { - LibAsset.transferFromERC20(tokenIn, msg.sender, router, amountIn); - } else if (from == address(this)) { - LibAsset.transferERC20(tokenIn, router, amountIn); - } - - // construct the path for V3 swap (tokenIn -> tokenOut with fee) - bytes memory path = abi.encodePacked(tokenIn, fee, tokenOut); - - // encode the inputs for V3_SWAP_EXACT_IN - // set payerIsUser to false since we already transferred tokens to the router - bytes[] memory inputs = new bytes[](1); - inputs[0] = abi.encode( - recipient, // recipient - amountIn, // amountIn - 0, // amountOutMin (0, as we handle slippage at higher level) - path, // path - false // payerIsUser (false since tokens are already in router) - ); - - // call the router's execute function - // first parameter for execute is the command for V3_SWAP_EXACT_IN (0x00) - IKatanaV3AggregateRouter(router).execute( - KATANA_V3_SWAP_EXACT_IN, - inputs - ); - - // katanaV3SwapCallback implementation is in the router contract itself - } - - /// @dev Common logic for iZiSwap callbacks - /// @param amountToPay The amount of tokens to be sent to the pool - /// @param data The data passed through by the caller - function _handleIzumiV3SwapCallback( - uint256 amountToPay, - bytes calldata data - ) private { - if (msg.sender != lastCalledPool) { - revert IzumiV3SwapCallbackUnknownSource(); - } - - if (amountToPay == 0) { - revert IzumiV3SwapCallbackNotPositiveAmount(); - } - - address tokenIn = abi.decode(data, (address)); - - // After a successful callback, we reset lastCalledPool to an impossible address. - // This prevents any subsequent callback from erroneously succeeding. - // It's a security measure to avoid reentrancy or misuse of stale state in future callbacks - lastCalledPool = IMPOSSIBLE_POOL_ADDRESS; - - IERC20(tokenIn).safeTransfer(msg.sender, amountToPay); - } - - /// @notice Called to `msg.sender` after executing a swap via IiZiSwapPool#swapX2Y - /// @dev In the implementation you must pay the pool tokens owed for the swap - /// @dev The caller of this method is checked against the `lastCalledPool` variable set during the swap call - /// This protects against unauthorized callbacks - /// @param amountX The amount of tokenX that must be sent to the pool by the end of the swap - /// @param {unused} The amount of tokenY that was sent by the pool in the swap - /// @param data Any data passed through by the caller via the IiZiSwapPool#swapX2Y call - function swapX2YCallback( - uint256 amountX, - uint256, - bytes calldata data - ) external { - _handleIzumiV3SwapCallback(amountX, data); - } - - /// @notice Called to `msg.sender` after executing a swap via IiZiSwapPool#swapY2X - /// @dev In the implementation you must pay the pool tokens owed for the swap - /// @dev The caller of this method is checked against the `lastCalledPool` variable set during the swap call - /// This protects against unauthorized callbacks - /// @param {unused} The amount of tokenX that was sent by the pool in the swap - /// @param amountY The amount of tokenY that must be sent to the pool by the end of the swap - /// @param data Any data passed through by the caller via the IiZiSwapPool#swapY2X call - function swapY2XCallback( - uint256, - uint256 amountY, - bytes calldata data - ) external { - // In swapY2X, we're swapping from tokenY to tokenX - // The pool will expect us to transfer the tokenY amount - _handleIzumiV3SwapCallback(amountY, data); - } - - /// @notice Called to `msg.sender` after executing a swap via HyperswapV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the HyperswapV3#swap call - function hyperswapV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via LaminarV3#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the LaminarV3#swap call - function laminarV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via IXSwapPool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method must be checked to be a XSwapPool deployed by the canonical XSwapFactory. - /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the IXSwapPoolActions#swap call - function xswapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via IRabbitSwapV3Pool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method must be checked to be a RabbitSwapV3Pool deployed by the canonical RabbitSwapV3Factory. - /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the IRabbitSwapV3PoolActions#swap call - function rabbitSwapV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Called to `msg.sender` after executing a swap via IEnosysDexV3Pool#swap. - /// @dev In the implementation you must pay the pool tokens owed for the swap. - /// The caller of this method must be checked to be a EnosysDexV3Pool deployed by the canonical EnosysDexV3Factory. - /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. - /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. - /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by - /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. - /// @param data Any data passed through by the caller via the IEnosysDexV3PoolActions#swap call - function enosysdexV3SwapCallback( - int256 amount0Delta, - int256 amount1Delta, - bytes calldata data - ) external { - uniswapV3SwapCallback(amount0Delta, amount1Delta, data); - } - - /// @notice Curve pool swap. Legacy pools that don't return amountOut and have native coins are not supported - /// @param stream [pool, poolType, fromIndex, toIndex, recipient, output token] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapCurve( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - uint8 poolType = stream.readUint8(); - int128 fromIndex = int8(stream.readUint8()); - int128 toIndex = int8(stream.readUint8()); - address to = stream.readAddress(); - address tokenOut = stream.readAddress(); - - uint256 amountOut; - if (tokenIn == NATIVE_ADDRESS) { - amountOut = ICurve(pool).exchange{ value: amountIn }( - fromIndex, - toIndex, - amountIn, - 0 - ); - } else { - if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom( - msg.sender, - address(this), - amountIn - ); - IERC20(tokenIn).approveSafe(pool, amountIn); - if (poolType == 0) - amountOut = ICurve(pool).exchange( - fromIndex, - toIndex, - amountIn, - 0 - ); - else { - uint256 balanceBefore = IERC20(tokenOut).balanceOf( - address(this) - ); - ICurveLegacy(pool).exchange(fromIndex, toIndex, amountIn, 0); - uint256 balanceAfter = IERC20(tokenOut).balanceOf( - address(this) - ); - amountOut = balanceAfter - balanceBefore; - } - } - - if (to != address(this)) { - if (tokenOut == NATIVE_ADDRESS) { - SafeTransferLib.safeTransferETH(to, amountOut); - } else { - IERC20(tokenOut).safeTransfer(to, amountOut); - } - } - } - - /// @notice Performs a swap through VelodromeV2 pools - /// @dev This function does not handle native token swaps directly, so processNative command cannot be used - /// @param stream [pool, direction, to, callback] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - function swapVelodromeV2( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - uint8 direction = stream.readUint8(); - address to = stream.readAddress(); - if (pool == address(0) || to == address(0)) revert InvalidCallData(); - // solhint-disable-next-line max-line-length - bool callback = stream.readUint8() == CALLBACK_ENABLED; // if true then run callback after swap with tokenIn as flashloan data. Will revert if contract (to) does not implement IVelodromeV2PoolCallee - - if (from == INTERNAL_INPUT_SOURCE) { - (uint256 reserve0, uint256 reserve1, ) = IVelodromeV2Pool(pool) - .getReserves(); - if (reserve0 == 0 || reserve1 == 0) revert WrongPoolReserves(); - uint256 reserveIn = direction == DIRECTION_TOKEN0_TO_TOKEN1 - ? reserve0 - : reserve1; - - amountIn = IERC20(tokenIn).balanceOf(pool) - reserveIn; - } else { - if (from == address(this)) - IERC20(tokenIn).safeTransfer(pool, amountIn); - else if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom(msg.sender, pool, amountIn); - } - - // calculate the expected output amount using the pool's getAmountOut function - uint256 amountOut = IVelodromeV2Pool(pool).getAmountOut( - amountIn, - tokenIn - ); - - // set the appropriate output amount based on which token is being swapped - // determine output amounts based on direction - uint256 amount0Out = direction == DIRECTION_TOKEN0_TO_TOKEN1 - ? 0 - : amountOut; - uint256 amount1Out = direction == DIRECTION_TOKEN0_TO_TOKEN1 - ? amountOut - : 0; - - // 'swap' function from IVelodromeV2Pool should be called from a contract which performs important safety checks. - // Safety Checks Covered: - // - Reentrancy: LDA has a custom lock() modifier - // - Token transfer safety: SafeERC20 is used to ensure token transfers revert on failure - // - Expected output verification: The contract calls getAmountOut (including fees) before executing the swap - // - Flashloan trigger: A flashloan flag is used to determine if the callback should be triggered - // - Post-swap verification: In processRouteInternal, it verifies that the recipient receives at least minAmountOut - // and that the sender's final balance is not less than the initial balance - // - Immutable interaction: Velodrome V2 pools and the router are not upgradable, - // so we can rely on the behavior of getAmountOut and swap - - // ATTENTION FOR CALLBACKS / HOOKS: - // - recipient contracts should validate that msg.sender is the Velodrome pool contract who is calling the hook - // - recipient contracts must not manipulate their own tokenOut balance - // (as this may bypass/invalidate the built-in slippage protection) - // - @developers: never trust balance-based slippage protection for callback recipients - // - @integrators: do not use slippage guarantees when recipient is a contract with side-effects - IVelodromeV2Pool(pool).swap( - amount0Out, - amount1Out, - to, - callback ? abi.encode(tokenIn) : bytes("") - ); - } - - /// @notice Algebra pool swap - /// @param stream [pool, direction, recipient, supportsFeeOnTransfer] - /// @param from Where to take liquidity for swap - /// @param tokenIn Input token - /// @param amountIn Amount of tokenIn to take for swap - /// @dev The supportsFeeOnTransfer flag accepts any non-zero value (1-255) to enable fee-on-transfer handling. - /// When enabled, the swap will first attempt to use swapSupportingFeeOnInputTokens(), and if that fails, - /// it will fall back to the regular swap() function. A value of 0 disables fee-on-transfer handling. - function swapAlgebra( - uint256 stream, - address from, - address tokenIn, - uint256 amountIn - ) private { - address pool = stream.readAddress(); - // solhint-disable-next-line max-line-length - bool direction = stream.readUint8() == DIRECTION_TOKEN0_TO_TOKEN1; // direction indicates the swap direction: true for token0 -> token1, false for token1 -> token0 - address recipient = stream.readAddress(); - bool supportsFeeOnTransfer = stream.readUint8() > 0; // Any non-zero value enables fee-on-transfer handling - - if ( - pool == address(0) || - pool == IMPOSSIBLE_POOL_ADDRESS || - recipient == address(0) - ) revert InvalidCallData(); - - if (from == msg.sender) - IERC20(tokenIn).safeTransferFrom( - msg.sender, - address(this), - uint256(amountIn) - ); - - lastCalledPool = pool; - - // Handle fee-on-transfer tokens with special care: - // - These tokens modify balances during transfer (fees, rebasing, etc.) - // - newest pool of Algebra versions has built-in support via swapSupportingFeeOnInputTokens() - // - Unlike UniswapV3, Algebra can safely handle these non-standard tokens. - if (supportsFeeOnTransfer) { - // If the pool is not using a version of Algebra that supports this feature, the swap will revert - // when attempting to use swapSupportingFeeOnInputTokens(), indicating the token was incorrectly - // flagged as fee-on-transfer or the pool doesn't support such tokens. - IAlgebraPool(pool).swapSupportingFeeOnInputTokens( - address(this), - recipient, - direction, - int256(amountIn), - direction ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, - abi.encode(tokenIn) - ); - } else { - IAlgebraPool(pool).swap( - recipient, - direction, - int256(amountIn), - direction ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, - abi.encode(tokenIn) - ); - } - - if (lastCalledPool != IMPOSSIBLE_POOL_ADDRESS) - revert AlgebraSwapUnexpected(); - } -} - -/// @notice Minimal BentoBox vault interface. -/// @dev `token` is aliased as `address` from `IERC20` for simplicity. -interface IBentoBoxMinimal { - /// @notice Balance per ERC-20 token per account in shares. - function balanceOf(address, address) external view returns (uint256); - - /// @dev Helper function to represent an `amount` of `token` in shares. - /// @param token The ERC-20 token. - /// @param amount The `token` amount. - /// @param roundUp If the result `share` should be rounded up. - /// @return share The token amount represented in shares. - function toShare( - address token, - uint256 amount, - bool roundUp - ) external view returns (uint256 share); - - /// @dev Helper function to represent shares back into the `token` amount. - /// @param token The ERC-20 token. - /// @param share The amount of shares. - /// @param roundUp If the result should be rounded up. - /// @return amount The share amount back into native representation. - function toAmount( - address token, - uint256 share, - bool roundUp - ) external view returns (uint256 amount); - - /// @notice Registers this contract so that users can approve it for BentoBox. - function registerProtocol() external; - - /// @notice Deposit an amount of `token` represented in either `amount` or `share`. - /// @param token The ERC-20 token to deposit. - /// @param from which account to pull the tokens. - /// @param to which account to push the tokens. - /// @param amount Token amount in native representation to deposit. - /// @param share Token amount represented in shares to deposit. Takes precedence over `amount`. - /// @return amountOut The amount deposited. - /// @return shareOut The deposited amount represented in shares. - function deposit( - address token, - address from, - address to, - uint256 amount, - uint256 share - ) external payable returns (uint256 amountOut, uint256 shareOut); - - /// @notice Withdraws an amount of `token` from a user account. - /// @param token_ The ERC-20 token to withdraw. - /// @param from which user to pull the tokens. - /// @param to which user to push the tokens. - /// @param amount of tokens. Either one of `amount` or `share` needs to be supplied. - /// @param share Like above, but `share` takes precedence over `amount`. - function withdraw( - address token_, - address from, - address to, - uint256 amount, - uint256 share - ) external returns (uint256 amountOut, uint256 shareOut); - - /// @notice Transfer shares from a user account to another one. - /// @param token The ERC-20 token to transfer. - /// @param from which user to pull the tokens. - /// @param to which user to push the tokens. - /// @param share The amount of `token` in shares. - function transfer( - address token, - address from, - address to, - uint256 share - ) external; - - /// @dev Reads the Rebase `totals`from storage for a given token - function totals(address token) external view returns (Rebase memory total); - - function strategyData( - address token - ) external view returns (StrategyData memory total); - - /// @dev Approves users' BentoBox assets to a "master" contract. - function setMasterContractApproval( - address user, - address masterContract, - bool approved, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - function harvest( - address token, - bool balance, - uint256 maxChangeAmount - ) external; -} - -interface ICurve { - function exchange( - int128 i, - int128 j, - uint256 dx, - // solhint-disable-next-line var-name-mixedcase - uint256 min_dy - ) external payable returns (uint256); -} - -interface ICurveLegacy { - function exchange( - int128 i, - int128 j, - uint256 dx, - // solhint-disable-next-line var-name-mixedcase - uint256 min_dy - ) external payable; -} - -/// @notice Trident pool interface. -interface IPool { - /// @notice Executes a swap from one token to another. - /// @dev The input tokens must've already been sent to the pool. - /// @param data ABI-encoded params that the pool requires. - /// @return finalAmountOut The amount of output tokens that were sent to the user. - function swap( - bytes calldata data - ) external returns (uint256 finalAmountOut); - - /// @notice Executes a swap from one token to another with a callback. - /// @dev This function allows borrowing the output tokens and sending the input tokens in the callback. - /// @param data ABI-encoded params that the pool requires. - /// @return finalAmountOut The amount of output tokens that were sent to the user. - function flashSwap( - bytes calldata data - ) external returns (uint256 finalAmountOut); - - /// @notice Mints liquidity tokens. - /// @param data ABI-encoded params that the pool requires. - /// @return liquidity The amount of liquidity tokens that were minted for the user. - function mint(bytes calldata data) external returns (uint256 liquidity); - - /// @notice Burns liquidity tokens. - /// @dev The input LP tokens must've already been sent to the pool. - /// @param data ABI-encoded params that the pool requires. - /// @return withdrawnAmounts The amount of various output tokens that were sent to the user. - function burn( - bytes calldata data - ) external returns (TokenAmount[] memory withdrawnAmounts); - - /// @notice Burns liquidity tokens for a single output token. - /// @dev The input LP tokens must've already been sent to the pool. - /// @param data ABI-encoded params that the pool requires. - /// @return amountOut The amount of output tokens that were sent to the user. - function burnSingle( - bytes calldata data - ) external returns (uint256 amountOut); - - /// @return A unique identifier for the pool type. - function poolIdentifier() external pure returns (bytes32); - - /// @return An array of tokens supported by the pool. - function getAssets() external view returns (address[] memory); - - /// @notice Simulates a trade and returns the expected output. - /// @dev The pool does not need to include a trade simulator directly in itself - it can use a library. - /// @param data ABI-encoded params that the pool requires. - /// @return finalAmountOut The amount of output tokens that will be sent to the user if the trade is executed. - function getAmountOut( - bytes calldata data - ) external view returns (uint256 finalAmountOut); - - /// @notice Simulates a trade and returns the expected output. - /// @dev The pool does not need to include a trade simulator directly in itself - it can use a library. - /// @param data ABI-encoded params that the pool requires. - /// @return finalAmountIn The amount of input tokens that are required from the user if the trade is executed. - function getAmountIn( - bytes calldata data - ) external view returns (uint256 finalAmountIn); - - /// @dev This event must be emitted on all swaps. - event Swap( - address indexed recipient, - address indexed tokenIn, - address indexed tokenOut, - uint256 amountIn, - uint256 amountOut - ); - - /// @dev This struct frames output tokens for burns. - struct TokenAmount { - address token; - uint256 amount; - } -} - -interface ITridentCLPool { - function token0() external returns (address); - - function token1() external returns (address); - - function swap( - address recipient, - bool zeroForOne, - int256 amountSpecified, - uint160 sqrtPriceLimitX96, - bool unwrapBento, - bytes calldata data - ) external returns (int256 amount0, int256 amount1); -} - -interface IUniswapV2Pair { - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); - event Transfer(address indexed from, address indexed to, uint256 value); - - function name() external pure returns (string memory); - - function symbol() external pure returns (string memory); - - function decimals() external pure returns (uint8); - - function totalSupply() external view returns (uint256); - - function balanceOf(address owner) external view returns (uint256); - - function allowance( - address owner, - address spender - ) external view returns (uint256); - - function approve(address spender, uint256 value) external returns (bool); - - function transfer(address to, uint256 value) external returns (bool); - - function transferFrom( - address from, - address to, - uint256 value - ) external returns (bool); - - function DOMAIN_SEPARATOR() external view returns (bytes32); - - function PERMIT_TYPEHASH() external pure returns (bytes32); - - function nonces(address owner) external view returns (uint256); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - event Mint(address indexed sender, uint256 amount0, uint256 amount1); - event Burn( - address indexed sender, - uint256 amount0, - uint256 amount1, - address indexed to - ); - event Swap( - address indexed sender, - uint256 amount0In, - uint256 amount1In, - uint256 amount0Out, - uint256 amount1Out, - address indexed to - ); - event Sync(uint112 reserve0, uint112 reserve1); - - function MINIMUM_LIQUIDITY() external pure returns (uint256); - - function factory() external view returns (address); - - function token0() external view returns (address); - - function token1() external view returns (address); - - function getReserves() - external - view - returns ( - uint112 reserve0, - uint112 reserve1, - uint32 blockTimestampLast - ); - - function price0CumulativeLast() external view returns (uint256); - - function price1CumulativeLast() external view returns (uint256); - - function kLast() external view returns (uint256); - - function mint(address to) external returns (uint256 liquidity); - - function burn( - address to - ) external returns (uint256 amount0, uint256 amount1); - - function swap( - uint256 amount0Out, - uint256 amount1Out, - address to, - bytes calldata data - ) external; - - function skim(address to) external; - - function sync() external; - - function initialize(address, address) external; -} - -interface IUniswapV3Pool { - function token0() external returns (address); - - function token1() external returns (address); - - function swap( - address recipient, - bool zeroForOne, - int256 amountSpecified, - uint160 sqrtPriceLimitX96, - bytes calldata data - ) external returns (int256 amount0, int256 amount1); -} - -interface IWETH { - function deposit() external payable; - - function transfer(address to, uint256 value) external returns (bool); - - function withdraw(uint256) external; -} - -/** @notice Simple read stream */ -library InputStream { - /** @notice Creates stream from data - * @param data data - */ - function createStream( - bytes memory data - ) internal pure returns (uint256 stream) { - assembly { - stream := mload(0x40) - mstore(0x40, add(stream, 64)) - mstore(stream, data) - let length := mload(data) - mstore(add(stream, 32), add(data, length)) - } - } - - /** @notice Checks if stream is not empty - * @param stream stream - */ - function isNotEmpty(uint256 stream) internal pure returns (bool) { - uint256 pos; - uint256 finish; - assembly { - pos := mload(stream) - finish := mload(add(stream, 32)) - } - return pos < finish; - } - - /** @notice Reads uint8 from the stream - * @param stream stream - */ - function readUint8(uint256 stream) internal pure returns (uint8 res) { - assembly { - let pos := mload(stream) - pos := add(pos, 1) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads uint16 from the stream - * @param stream stream - */ - function readUint16(uint256 stream) internal pure returns (uint16 res) { - assembly { - let pos := mload(stream) - pos := add(pos, 2) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads uint24 from the stream - * @param stream stream - */ - function readUint24(uint256 stream) internal pure returns (uint24 res) { - assembly { - let pos := mload(stream) - pos := add(pos, 3) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads uint32 from the stream - * @param stream stream - */ - function readUint32(uint256 stream) internal pure returns (uint32 res) { - assembly { - let pos := mload(stream) - pos := add(pos, 4) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads uint256 from the stream - * @param stream stream - */ - function readUint(uint256 stream) internal pure returns (uint256 res) { - assembly { - let pos := mload(stream) - pos := add(pos, 32) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads bytes32 from the stream - * @param stream stream - */ - function readBytes32(uint256 stream) internal pure returns (bytes32 res) { - assembly { - let pos := mload(stream) - pos := add(pos, 32) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads address from the stream - * @param stream stream - */ - function readAddress(uint256 stream) internal pure returns (address res) { - assembly { - let pos := mload(stream) - pos := add(pos, 20) - res := mload(pos) - mstore(stream, pos) - } - } - - /** @notice Reads bytes from the stream - * @param stream stream - */ - function readBytes( - uint256 stream - ) internal pure returns (bytes memory res) { - assembly { - let pos := mload(stream) - res := add(pos, 32) - let length := mload(res) - mstore(stream, add(res, length)) - } - } -} - -library Approve { - /** - * @dev ERC20 approve that correct works with token.approve which returns bool or nothing (USDT for example) - * @param token The token targeted by the call. - * @param spender token spender - * @param amount token amount - */ - function approveStable( - IERC20 token, - address spender, - uint256 amount - ) internal returns (bool) { - (bool success, bytes memory data) = address(token).call( - abi.encodeWithSelector(token.approve.selector, spender, amount) - ); - return success && (data.length == 0 || abi.decode(data, (bool))); - } - - /** - * @dev ERC20 approve that correct works with token.approve which reverts if amount and - * current allowance are not zero simultaniously (USDT for example). - * In second case it tries to set allowance to 0, and then back to amount. - * @param token The token targeted by the call. - * @param spender token spender - * @param amount token amount - */ - function approveSafe( - IERC20 token, - address spender, - uint256 amount - ) internal returns (bool) { - return - approveStable(token, spender, amount) || - (approveStable(token, spender, 0) && - approveStable(token, spender, amount)); - } -} - -struct Rebase { - uint128 elastic; - uint128 base; -} - -struct StrategyData { - uint64 strategyStartDate; - uint64 targetPercentage; - uint128 balance; // the balance of the strategy that BentoBox thinks is in there -} - -/// @notice A rebasing library -library RebaseLibrary { - /// @notice Calculates the base value in relationship to `elastic` and `total`. - function toBase( - Rebase memory total, - uint256 elastic - ) internal pure returns (uint256 base) { - if (total.elastic == 0) { - base = elastic; - } else { - base = (elastic * total.base) / total.elastic; - } - } - - /// @notice Calculates the elastic value in relationship to `base` and `total`. - function toElastic( - Rebase memory total, - uint256 base - ) internal pure returns (uint256 elastic) { - if (total.base == 0) { - elastic = base; - } else { - elastic = (base * total.elastic) / total.base; - } - } -} diff --git a/templates/facetTest.template.hbs b/templates/facetTest.template.hbs index eb29a303f..9b6f6c29c 100644 --- a/templates/facetTest.template.hbs +++ b/templates/facetTest.template.hbs @@ -46,7 +46,7 @@ contract {{titleCase name}}FacetTest is TestBaseFacet { .setFunctionWhitelistBySelector .selector; - addFacet(diamond, address({{camelCase name}}Facet), functionSelectors); + addFacet(address(diamond), address({{camelCase name}}Facet), functionSelectors); {{camelCase name}}Facet = Test{{titleCase name}}Facet(address(diamond)); {{camelCase name}}Facet.addToWhitelist(ADDRESS_UNISWAP); {{camelCase name}}Facet.setFunctionWhitelistBySelector( diff --git a/test/solidity/Facets/AccessManagerFacet.t.sol b/test/solidity/Facets/AccessManagerFacet.t.sol index c2308850d..ddba693c4 100644 --- a/test/solidity/Facets/AccessManagerFacet.t.sol +++ b/test/solidity/Facets/AccessManagerFacet.t.sol @@ -18,7 +18,7 @@ contract AccessManagerFacetTest is TestBase { AccessManagerFacet internal accessMgr; RestrictedContract internal restricted; - function setUp() public { + function setUp() public override { initTestBase(); accessMgr = new AccessManagerFacet(); @@ -29,11 +29,19 @@ contract AccessManagerFacetTest is TestBase { allowedFunctionSelectors[1] = accessMgr .addressCanExecuteMethod .selector; - addFacet(diamond, address(accessMgr), allowedFunctionSelectors); + addFacet( + address(diamond), + address(accessMgr), + allowedFunctionSelectors + ); bytes4[] memory restrictedFunctionSelectors = new bytes4[](1); restrictedFunctionSelectors[0] = restricted.restrictedMethod.selector; - addFacet(diamond, address(restricted), restrictedFunctionSelectors); + addFacet( + address(diamond), + address(restricted), + restrictedFunctionSelectors + ); accessMgr = AccessManagerFacet(address(diamond)); restricted = RestrictedContract(address(diamond)); diff --git a/test/solidity/Facets/Across/V1/AcrossFacet.t.sol b/test/solidity/Facets/Across/V1/AcrossFacet.t.sol index 28f55cc2e..5dec83756 100644 --- a/test/solidity/Facets/Across/V1/AcrossFacet.t.sol +++ b/test/solidity/Facets/Across/V1/AcrossFacet.t.sol @@ -27,7 +27,7 @@ contract AcrossFacetTest is TestBaseFacet { AcrossFacet.AcrossData internal validAcrossData; TestAcrossFacet internal acrossFacet; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 17130542; initTestBase(); @@ -39,7 +39,7 @@ contract AcrossFacetTest is TestBaseFacet { .selector; functionSelectors[2] = acrossFacet.addAllowedContractSelector.selector; - addFacet(diamond, address(acrossFacet), functionSelectors); + addFacet(address(diamond), address(acrossFacet), functionSelectors); acrossFacet = TestAcrossFacet(address(diamond)); acrossFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/Across/V1/AcrossFacetPacked.t.sol b/test/solidity/Facets/Across/V1/AcrossFacetPacked.t.sol index df731fcf1..459898fab 100644 --- a/test/solidity/Facets/Across/V1/AcrossFacetPacked.t.sol +++ b/test/solidity/Facets/Across/V1/AcrossFacetPacked.t.sol @@ -1,16 +1,15 @@ // // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AcrossFacet } from "lifi/Facets/AcrossFacet.sol"; import { AcrossFacetPacked } from "lifi/Facets/AcrossFacetPacked.sol"; import { IAcrossSpokePool } from "lifi/Interfaces/IAcrossSpokePool.sol"; import { LibAsset, IERC20 } from "lifi/Libraries/LibAsset.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { UnAuthorized } from "lifi/Errors/GenericErrors.sol"; import { TestBase } from "../../../utils/TestBase.sol"; import { MockFailingContract } from "../../../utils/MockFailingContract.sol"; -import { UnAuthorized } from "src/Errors/GenericErrors.sol"; - contract TestClaimContract { using SafeERC20 for IERC20; @@ -58,7 +57,7 @@ contract AcrossFacetPackedTest is TestBase { event LiFiAcrossTransfer(bytes8 _transactionId); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19145375; initTestBase(); @@ -106,7 +105,11 @@ contract AcrossFacetPackedTest is TestBase { .selector; // add facet to diamond - addFacet(diamond, address(acrossFacetPacked), functionSelectors); + addFacet( + address(diamond), + address(acrossFacetPacked), + functionSelectors + ); acrossFacetPacked = AcrossFacetPacked(payable(address(diamond))); /// Prepare parameters diff --git a/test/solidity/Facets/Across/V3/AcrossFacetPackedV3.t.sol b/test/solidity/Facets/Across/V3/AcrossFacetPackedV3.t.sol index 4c34f9ce3..44087b814 100644 --- a/test/solidity/Facets/Across/V3/AcrossFacetPackedV3.t.sol +++ b/test/solidity/Facets/Across/V3/AcrossFacetPackedV3.t.sol @@ -1,14 +1,14 @@ // // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AcrossFacetV3 } from "lifi/Facets/AcrossFacetV3.sol"; import { AcrossFacetPackedV3 } from "lifi/Facets/AcrossFacetPackedV3.sol"; import { IAcrossSpokePool } from "lifi/Interfaces/IAcrossSpokePool.sol"; import { LibAsset, IERC20 } from "lifi/Libraries/LibAsset.sol"; import { LibUtil } from "lifi/Libraries/LibUtil.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { UnAuthorized } from "lifi/Errors/GenericErrors.sol"; import { TestBase } from "../../../utils/TestBase.sol"; -import { UnAuthorized } from "src/Errors/GenericErrors.sol"; contract TestClaimContract { using SafeERC20 for IERC20; @@ -63,7 +63,7 @@ contract AcrossFacetPackedV3Test is TestBase { event LiFiAcrossTransfer(bytes8 _transactionId); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19960294; initTestBase(); @@ -113,7 +113,11 @@ contract AcrossFacetPackedV3Test is TestBase { .selector; // add facet to diamond - addFacet(diamond, address(acrossFacetPackedV3), functionSelectors); + addFacet( + address(diamond), + address(acrossFacetPackedV3), + functionSelectors + ); acrossFacetPackedV3 = AcrossFacetPackedV3(payable(address(diamond))); /// Prepare parameters diff --git a/test/solidity/Facets/Across/V3/AcrossFacetV3.t.sol b/test/solidity/Facets/Across/V3/AcrossFacetV3.t.sol index f165becf9..821775046 100644 --- a/test/solidity/Facets/Across/V3/AcrossFacetV3.t.sol +++ b/test/solidity/Facets/Across/V3/AcrossFacetV3.t.sol @@ -31,7 +31,7 @@ contract AcrossFacetV3Test is TestBaseFacet { error InvalidQuoteTimestamp(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19960294; initTestBase(); @@ -47,7 +47,7 @@ contract AcrossFacetV3Test is TestBaseFacet { .addAllowedContractSelector .selector; - addFacet(diamond, address(acrossFacetV3), functionSelectors); + addFacet(address(diamond), address(acrossFacetV3), functionSelectors); acrossFacetV3 = TestAcrossFacetV3(address(diamond)); acrossFacetV3.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/Across/V4/AcrossFacetPackedV4.t.sol b/test/solidity/Facets/Across/V4/AcrossFacetPackedV4.t.sol index 30d9aaedb..668f0fa8b 100644 --- a/test/solidity/Facets/Across/V4/AcrossFacetPackedV4.t.sol +++ b/test/solidity/Facets/Across/V4/AcrossFacetPackedV4.t.sol @@ -73,7 +73,7 @@ contract AcrossFacetPackedV4Test is TestBase { event LiFiAcrossTransfer(bytes8 _transactionId); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22993652; initTestBase(); @@ -123,7 +123,11 @@ contract AcrossFacetPackedV4Test is TestBase { .selector; // add facet to diamond - addFacet(diamond, address(acrossFacetPackedV4), functionSelectors); + addFacet( + address(diamond), + address(acrossFacetPackedV4), + functionSelectors + ); acrossFacetPackedV4 = AcrossFacetPackedV4(payable(address(diamond))); /// Prepare parameters diff --git a/test/solidity/Facets/Across/V4/AcrossFacetV4.t.sol b/test/solidity/Facets/Across/V4/AcrossFacetV4.t.sol index 746009278..903c1b0c2 100644 --- a/test/solidity/Facets/Across/V4/AcrossFacetV4.t.sol +++ b/test/solidity/Facets/Across/V4/AcrossFacetV4.t.sol @@ -32,7 +32,7 @@ contract AcrossFacetV4Test is TestBaseFacet { error InvalidQuoteTimestamp(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22989702; initTestBase(); @@ -48,7 +48,7 @@ contract AcrossFacetV4Test is TestBaseFacet { .addAllowedContractSelector .selector; - addFacet(diamond, address(acrossFacetV4), functionSelectors); + addFacet(address(diamond), address(acrossFacetV4), functionSelectors); acrossFacetV4 = TestAcrossFacetV4(address(diamond)); acrossFacetV4.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountCalculation.t.sol b/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountCalculation.t.sol index bf46c8eb4..574b6e6d4 100644 --- a/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountCalculation.t.sol +++ b/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountCalculation.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { TestBaseFacet } from "../../../utils/TestBaseFacet.sol"; -import { TestHelpers, MockUniswapDEX } from "../../../utils/TestHelpers.sol"; +import { MockUniswapDEX } from "../../../utils/TestHelpers.sol"; import { AcrossFacetV4 } from "lifi/Facets/AcrossFacetV4.sol"; import { IAcrossSpokePoolV4 } from "lifi/Interfaces/IAcrossSpokePoolV4.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; @@ -23,10 +23,7 @@ contract TestAcrossFacetV4 is AcrossFacetV4, TestWhitelistManagerBase { /// @author LI.FI (https://li.fi) /// @notice Integration tests for AcrossFacetV4 outputAmount calculation with mock DEX /// @custom:version 1.0.0 -contract AcrossFacetV4OutputAmountIntegrationTest is - TestBaseFacet, - TestHelpers -{ +contract AcrossFacetV4OutputAmountIntegrationTest is TestBaseFacet { address internal constant SPOKE_POOL = 0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5; @@ -39,7 +36,7 @@ contract AcrossFacetV4OutputAmountIntegrationTest is error InvalidQuoteTimestamp(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22989702; initTestBase(); @@ -59,7 +56,7 @@ contract AcrossFacetV4OutputAmountIntegrationTest is .addAllowedContractSelector .selector; - addFacet(diamond, address(acrossFacetV4), functionSelectors); + addFacet(address(diamond), address(acrossFacetV4), functionSelectors); acrossFacetV4 = TestAcrossFacetV4(address(diamond)); setFacetAddressInTestBase(address(acrossFacetV4), "AcrossFacetV4"); diff --git a/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountIntegration.t.sol b/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountIntegration.t.sol index bebd1c45e..001eb5236 100644 --- a/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountIntegration.t.sol +++ b/test/solidity/Facets/Across/V4/AcrossFacetV4OutputAmountIntegration.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { TestBaseFacet } from "../../../utils/TestBaseFacet.sol"; -import { TestHelpers, MockUniswapDEX } from "../../../utils/TestHelpers.sol"; +import { MockUniswapDEX } from "../../../utils/TestHelpers.sol"; import { AcrossFacetV4 } from "lifi/Facets/AcrossFacetV4.sol"; import { IAcrossSpokePoolV4 } from "lifi/Interfaces/IAcrossSpokePoolV4.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; @@ -23,10 +23,7 @@ contract TestAcrossFacetV4 is AcrossFacetV4, TestWhitelistManagerBase { /// @author LI.FI (https://li.fi) /// @notice Integration tests for AcrossFacetV4 outputAmount calculation with mock DEX /// @custom:version 1.0.0 -contract AcrossFacetV4OutputAmountIntegrationTest is - TestBaseFacet, - TestHelpers -{ +contract AcrossFacetV4OutputAmountIntegrationTest is TestBaseFacet { address internal constant SPOKE_POOL = 0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5; @@ -39,7 +36,7 @@ contract AcrossFacetV4OutputAmountIntegrationTest is error InvalidQuoteTimestamp(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22989702; initTestBase(); @@ -59,7 +56,7 @@ contract AcrossFacetV4OutputAmountIntegrationTest is .addAllowedContractSelector .selector; - addFacet(diamond, address(acrossFacetV4), functionSelectors); + addFacet(address(diamond), address(acrossFacetV4), functionSelectors); acrossFacetV4 = TestAcrossFacetV4(address(diamond)); setFacetAddressInTestBase(address(acrossFacetV4), "AcrossFacetV4"); diff --git a/test/solidity/Facets/AllBridgeFacet.t.sol b/test/solidity/Facets/AllBridgeFacet.t.sol index 44889b2d5..3f6054757 100644 --- a/test/solidity/Facets/AllBridgeFacet.t.sol +++ b/test/solidity/Facets/AllBridgeFacet.t.sol @@ -55,7 +55,7 @@ contract AllBridgeFacetTest is TestBaseFacet { AllBridgeFacet.AllBridgeData internal validAllBridgeData; TestAllBridgeFacet internal allBridgeFacet; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 17556456; initTestBase(); @@ -72,7 +72,7 @@ contract AllBridgeFacetTest is TestBaseFacet { .selector; functionSelectors[3] = allBridgeFacet.getAllBridgeChainId.selector; - addFacet(diamond, address(allBridgeFacet), functionSelectors); + addFacet(address(diamond), address(allBridgeFacet), functionSelectors); allBridgeFacet = TestAllBridgeFacet(address(diamond)); allBridgeFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/ArbitrumBridgeFacet.t.sol b/test/solidity/Facets/ArbitrumBridgeFacet.t.sol index c67fba6f0..90c7e6656 100644 --- a/test/solidity/Facets/ArbitrumBridgeFacet.t.sol +++ b/test/solidity/Facets/ArbitrumBridgeFacet.t.sol @@ -32,7 +32,7 @@ contract ArbitrumBridgeFacetTest is TestBaseFacet { ArbitrumBridgeFacet.ArbitrumData internal arbitrumData; uint256 internal cost; - function setUp() public { + function setUp() public override { initTestBase(); arbitrumBridgeFacet = new TestArbitrumBridgeFacet( @@ -47,15 +47,30 @@ contract ArbitrumBridgeFacetTest is TestBaseFacet { functionSelectors[1] = arbitrumBridgeFacet .swapAndStartBridgeTokensViaArbitrumBridge .selector; - functionSelectors[2] = arbitrumBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = arbitrumBridgeFacet + .addAllowedContractSelector + .selector; - addFacet(diamond, address(arbitrumBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(arbitrumBridgeFacet), + functionSelectors + ); arbitrumBridgeFacet = TestArbitrumBridgeFacet(address(diamond)); - arbitrumBridgeFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - arbitrumBridgeFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); - arbitrumBridgeFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); + arbitrumBridgeFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + arbitrumBridgeFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); + arbitrumBridgeFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); setFacetAddressInTestBase( address(arbitrumBridgeFacet), diff --git a/test/solidity/Facets/CBridge/CBridge.t.sol b/test/solidity/Facets/CBridge/CBridge.t.sol index dba61c713..5d64372fc 100644 --- a/test/solidity/Facets/CBridge/CBridge.t.sol +++ b/test/solidity/Facets/CBridge/CBridge.t.sol @@ -69,7 +69,7 @@ contract CBridgeFacetTest is TestBaseFacet { } } - function setUp() public { + function setUp() public override { initTestBase(); cBridge = new TestCBridgeFacet(ICBridge(CBRIDGE_ROUTER)); bytes4[] memory functionSelectors = new bytes4[](4); @@ -80,12 +80,21 @@ contract CBridgeFacetTest is TestBaseFacet { functionSelectors[2] = cBridge.addAllowedContractSelector.selector; functionSelectors[3] = cBridge.triggerRefund.selector; - addFacet(diamond, address(cBridge), functionSelectors); + addFacet(address(diamond), address(cBridge), functionSelectors); cBridge = TestCBridgeFacet(address(diamond)); - cBridge.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - cBridge.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); - cBridge.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); + cBridge.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + cBridge.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); + cBridge.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); setFacetAddressInTestBase(address(cBridge), "cBridgeFacet"); } diff --git a/test/solidity/Facets/CBridge/CBridgeAndFeeCollection.t.sol b/test/solidity/Facets/CBridge/CBridgeAndFeeCollection.t.sol index 9c2ca53a3..80b660bba 100644 --- a/test/solidity/Facets/CBridge/CBridgeAndFeeCollection.t.sol +++ b/test/solidity/Facets/CBridge/CBridgeAndFeeCollection.t.sol @@ -21,7 +21,7 @@ contract CBridgeAndFeeCollectionTest is TestBase { TestCBridgeFacet internal cBridge; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 14847528; initTestBase(); @@ -34,14 +34,29 @@ contract CBridgeAndFeeCollectionTest is TestBase { .selector; functionSelectors[2] = cBridge.addAllowedContractSelector.selector; - addFacet(diamond, address(cBridge), functionSelectors); + addFacet(address(diamond), address(cBridge), functionSelectors); cBridge = TestCBridgeFacet(address(diamond)); - cBridge.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - cBridge.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); - cBridge.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); - cBridge.addAllowedContractSelector(address(feeCollector), feeCollector.collectTokenFees.selector); - cBridge.addAllowedContractSelector(address(feeCollector), feeCollector.collectNativeFees.selector); + cBridge.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + cBridge.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); + cBridge.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); + cBridge.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectTokenFees.selector + ); + cBridge.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectNativeFees.selector + ); } function testCanCollectTokenFeesAndBridgeTokens() public { diff --git a/test/solidity/Facets/CBridge/CBridgeFacetPacked.t.sol b/test/solidity/Facets/CBridge/CBridgeFacetPacked.t.sol index 5330e87f4..3bbdc503e 100644 --- a/test/solidity/Facets/CBridge/CBridgeFacetPacked.t.sol +++ b/test/solidity/Facets/CBridge/CBridgeFacetPacked.t.sol @@ -1,14 +1,14 @@ // // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; +import { ERC20 } from "solmate/tokens/ERC20.sol"; import { ICBridge } from "lifi/Interfaces/ICBridge.sol"; import { CBridgeFacet } from "lifi/Facets/CBridgeFacet.sol"; -import { ERC20 } from "solmate/tokens/ERC20.sol"; import { CBridgeFacetPacked } from "lifi/Facets/CBridgeFacetPacked.sol"; import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; +import { ContractCallNotAllowed, ExternalCallFailed, UnAuthorized } from "lifi/Errors/GenericErrors.sol"; import { TestBase } from "../../utils/TestBase.sol"; import { MockLiquidityBridge } from "../../utils/MockLiquidityBridge.sol"; -import { ContractCallNotAllowed, ExternalCallFailed, UnAuthorized } from "lifi/Errors/GenericErrors.sol"; contract CBridgeFacetPackedTest is TestBase { address internal constant CBRIDGE_ROUTER = @@ -46,7 +46,7 @@ contract CBridgeFacetPackedTest is TestBase { uint256 amount ); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 58467500; customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; initTestBase(); @@ -86,7 +86,11 @@ contract CBridgeFacetPackedTest is TestBase { .selector; functionSelectors[8] = cBridgeFacetPacked.triggerRefund.selector; - addFacet(diamond, address(cBridgeFacetPacked), functionSelectors); + addFacet( + address(diamond), + address(cBridgeFacetPacked), + functionSelectors + ); cBridgeFacetPacked = CBridgeFacetPacked(payable(address(diamond))); /// Perpare parameters diff --git a/test/solidity/Facets/CBridge/CBridgeRefund.t.sol b/test/solidity/Facets/CBridge/CBridgeRefund.t.sol index 96b3faecb..1802037b1 100644 --- a/test/solidity/Facets/CBridge/CBridgeRefund.t.sol +++ b/test/solidity/Facets/CBridge/CBridgeRefund.t.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.17; import { DSTest } from "ds-test/test.sol"; -import { DiamondTest, LiFiDiamond } from "../../utils/DiamondTest.sol"; +import { DiamondTestHelpers } from "../../utils/DiamondTestHelpers.sol"; +import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { WithdrawFacet } from "lifi/Facets/WithdrawFacet.sol"; import { UnAuthorized, NotAContract } from "lifi/Errors/GenericErrors.sol"; @@ -11,7 +12,7 @@ import { UnAuthorized, NotAContract } from "lifi/Errors/GenericErrors.sol"; // Actual refund was processed at 25085299(Feb-18-2022 03:24:09 PM +UTC) // Run `forge test --match-path test\solidity\Facets\CBridgeRefund.t.sol --fork-url POLYGON_RPC_URL --fork-block-number 25085298` // or `forge test --match-contract CBridgeRefundTest --fork-url POLYGON_RPC_URL --fork-block-number 25085298` -contract CBridgeRefundTestPolygon is DSTest, DiamondTest { +contract CBridgeRefundTestPolygon is DSTest, DiamondTestHelpers { address internal constant CBRIDGE_ADDRESS = 0x88DCDC47D2f83a99CF0000FDF667A468bB958a78; address internal constant LIFI_ADDRESS = @@ -29,7 +30,6 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { error WithdrawFailed(); bytes internal validCalldata; - LiFiDiamond internal diamond; WithdrawFacet internal withdrawFacet; @@ -93,7 +93,7 @@ contract CBridgeRefundTestPolygon is DSTest, DiamondTest { selector[0] = withdrawFacet.executeCallAndWithdraw.selector; vm.startPrank(OWNER_ADDRESS); - addFacet(diamond, address(withdrawFacet), selector); + addFacet(address(diamond), address(withdrawFacet), selector); vm.stopPrank(); withdrawFacet = WithdrawFacet(address(diamond)); diff --git a/test/solidity/Facets/CalldataVerificationFacet.t.sol b/test/solidity/Facets/CalldataVerificationFacet.t.sol index d51cbe3c5..a81b003ff 100644 --- a/test/solidity/Facets/CalldataVerificationFacet.t.sol +++ b/test/solidity/Facets/CalldataVerificationFacet.t.sol @@ -22,7 +22,7 @@ contract CalldataVerificationFacetTest is TestBase { error SliceOutOfBounds(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19979843; initTestBase(); calldataVerificationFacet = new CalldataVerificationFacet(); @@ -58,7 +58,7 @@ contract CalldataVerificationFacetTest is TestBase { ); } - function test_DeploysWithoutErrors() public { + function test_DeploysWithoutErrors() public virtual override { calldataVerificationFacet = new CalldataVerificationFacet(); } diff --git a/test/solidity/Facets/CelerCircleBridgeFacet.t.sol b/test/solidity/Facets/CelerCircleBridgeFacet.t.sol index aedbc4418..a467a0441 100644 --- a/test/solidity/Facets/CelerCircleBridgeFacet.t.sol +++ b/test/solidity/Facets/CelerCircleBridgeFacet.t.sol @@ -23,7 +23,7 @@ contract CelerCircleBridgeFacetTest is TestBaseFacet { TestCelerCircleBridgeFacet internal celerCircleBridgeFacet; - function setUp() public { + function setUp() public override { // Custom Config customBlockNumberForForking = 17118891; // after proxy+bridge configuration @@ -44,15 +44,30 @@ contract CelerCircleBridgeFacetTest is TestBaseFacet { functionSelectors[1] = celerCircleBridgeFacet .swapAndStartBridgeTokensViaCelerCircleBridge .selector; - functionSelectors[2] = celerCircleBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = celerCircleBridgeFacet + .addAllowedContractSelector + .selector; - addFacet(diamond, address(celerCircleBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(celerCircleBridgeFacet), + functionSelectors + ); celerCircleBridgeFacet = TestCelerCircleBridgeFacet(address(diamond)); - celerCircleBridgeFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - celerCircleBridgeFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForETH.selector); - celerCircleBridgeFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); + celerCircleBridgeFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + celerCircleBridgeFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForETH.selector + ); + celerCircleBridgeFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); setFacetAddressInTestBase( address(celerCircleBridgeFacet), diff --git a/test/solidity/Facets/ChainflipFacet.t.sol b/test/solidity/Facets/ChainflipFacet.t.sol index e839522df..a95f9daef 100644 --- a/test/solidity/Facets/ChainflipFacet.t.sol +++ b/test/solidity/Facets/ChainflipFacet.t.sol @@ -27,7 +27,7 @@ contract ChainflipFacetTest is TestBaseFacet { uint256 internal constant CHAIN_ID_SOLANA = 1151111081099710; uint256 internal constant CHAIN_ID_BITCOIN = 20000000000001; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 18277082; initTestBase(); @@ -50,7 +50,7 @@ contract ChainflipFacetTest is TestBaseFacet { .addAllowedContractSelector .selector; - addFacet(diamond, address(chainflipFacet), functionSelectors); + addFacet(address(diamond), address(chainflipFacet), functionSelectors); chainflipFacet = TestChainflipFacet(address(diamond)); chainflipFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/DeBridgeDlnFacet.t.sol b/test/solidity/Facets/DeBridgeDlnFacet.t.sol index a2a3665a5..df180e758 100644 --- a/test/solidity/Facets/DeBridgeDlnFacet.t.sol +++ b/test/solidity/Facets/DeBridgeDlnFacet.t.sol @@ -34,7 +34,7 @@ contract DeBridgeDlnFacetTest is TestBaseFacet { bytes32 internal namespace = keccak256("com.lifi.facets.debridgedln"); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19279222; initTestBase(); @@ -46,16 +46,31 @@ contract DeBridgeDlnFacetTest is TestBaseFacet { functionSelectors[1] = deBridgeDlnFacet .swapAndStartBridgeTokensViaDeBridgeDln .selector; - functionSelectors[2] = deBridgeDlnFacet.addAllowedContractSelector.selector; + functionSelectors[2] = deBridgeDlnFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = deBridgeDlnFacet.setDeBridgeChainId.selector; functionSelectors[4] = deBridgeDlnFacet.getDeBridgeChainId.selector; functionSelectors[5] = DeBridgeDlnFacet.initDeBridgeDln.selector; - addFacet(diamond, address(deBridgeDlnFacet), functionSelectors); + addFacet( + address(diamond), + address(deBridgeDlnFacet), + functionSelectors + ); deBridgeDlnFacet = TestDeBridgeDlnFacet(address(diamond)); - deBridgeDlnFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - deBridgeDlnFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); - deBridgeDlnFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); + deBridgeDlnFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + deBridgeDlnFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); + deBridgeDlnFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); // Initialize string memory path = string.concat( diff --git a/test/solidity/Facets/EcoFacet.t.sol b/test/solidity/Facets/EcoFacet.t.sol index 532117697..9895a55cf 100644 --- a/test/solidity/Facets/EcoFacet.t.sol +++ b/test/solidity/Facets/EcoFacet.t.sol @@ -22,7 +22,7 @@ contract EcoFacetTest is TestBaseFacet { 0xB5e58A8206473Df3Ab9b8DDd3B0F84c0ba68F8b5; uint256 internal constant TOKEN_SOLVER_REWARD = 10 * 10 ** 6; // 10 USDC (6 decimals) - function setUp() public { + function setUp() public virtual override { customBlockNumberForForking = 35717845; customRpcUrlForForking = "ETH_NODE_URI_BASE"; initTestBase(); @@ -48,7 +48,7 @@ contract EcoFacetTest is TestBaseFacet { .selector; functionSelectors[2] = ecoFacet.addAllowedContractSelector.selector; - addFacet(diamond, address(ecoFacet), functionSelectors); + addFacet(address(diamond), address(ecoFacet), functionSelectors); ecoFacet = TestEcoFacet(address(diamond)); ecoFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.fork.t.sol b/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.fork.t.sol index 91d5dafd5..d28f7d3ae 100644 --- a/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.fork.t.sol +++ b/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.fork.t.sol @@ -33,27 +33,33 @@ contract EmergencyPauseFacetPRODTest is TestBase { 0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE; address internal constant USER_DIAMOND_OWNER_MAINNET = 0x37347dD595C49212C5FC2D95EA10d1085896f51E; - TestEmergencyPauseFacet internal emergencyPauseFacet; + TestEmergencyPauseFacet internal emergencyPauseFacetExtended; address[] internal blacklist = new address[](0); - function setUp() public { + function setUp() public override { // set custom block number for forking customBlockNumberForForking = 19979843; initTestBase(); // deploy EmergencyPauseFacet - emergencyPauseFacet = new TestEmergencyPauseFacet(USER_PAUSER); + emergencyPauseFacetExtended = new TestEmergencyPauseFacet(USER_PAUSER); // prepare diamondCut bytes4[] memory functionSelectors = new bytes4[](3); - functionSelectors[0] = emergencyPauseFacet.removeFacet.selector; - functionSelectors[1] = emergencyPauseFacet.pauseDiamond.selector; - functionSelectors[2] = emergencyPauseFacet.unpauseDiamond.selector; + functionSelectors[0] = emergencyPauseFacetExtended + .removeFacet + .selector; + functionSelectors[1] = emergencyPauseFacetExtended + .pauseDiamond + .selector; + functionSelectors[2] = emergencyPauseFacetExtended + .unpauseDiamond + .selector; cut.push( LibDiamond.FacetCut({ - facetAddress: address(emergencyPauseFacet), + facetAddress: address(emergencyPauseFacetExtended), action: LibDiamond.FacetCutAction.Add, functionSelectors: functionSelectors }) @@ -68,13 +74,13 @@ contract EmergencyPauseFacetPRODTest is TestBase { ); // store diamond in local TestEmergencyPauseFacet variable - emergencyPauseFacet = TestEmergencyPauseFacet( + emergencyPauseFacetExtended = TestEmergencyPauseFacet( payable(address(ADDRESS_DIAMOND_MAINNET)) ); // set facet address in TestBase setFacetAddressInTestBase( - address(emergencyPauseFacet), + address(emergencyPauseFacetExtended), "EmergencyPauseFacet" ); @@ -83,44 +89,56 @@ contract EmergencyPauseFacetPRODTest is TestBase { function test_PauserWalletCanPauseDiamond() public { vm.startPrank(USER_PAUSER); - vm.expectEmit(true, true, true, true, address(emergencyPauseFacet)); + vm.expectEmit( + true, + true, + true, + true, + address(emergencyPauseFacetExtended) + ); emit EmergencyPaused(USER_PAUSER); // pause the contract - emergencyPauseFacet.pauseDiamond(); + emergencyPauseFacetExtended.pauseDiamond(); // try to get a list of all registered facets via DiamondLoupe vm.expectRevert(DiamondIsPaused.selector); - DiamondLoupeFacet(address(emergencyPauseFacet)).facets(); + DiamondLoupeFacet(address(emergencyPauseFacetExtended)).facets(); } function test_DiamondOwnerCanPauseDiamond() public { vm.startPrank(USER_DIAMOND_OWNER_MAINNET); - vm.expectEmit(true, true, true, true, address(emergencyPauseFacet)); + vm.expectEmit( + true, + true, + true, + true, + address(emergencyPauseFacetExtended) + ); emit EmergencyPaused(USER_DIAMOND_OWNER_MAINNET); // pause the contract - emergencyPauseFacet.pauseDiamond(); + emergencyPauseFacetExtended.pauseDiamond(); // try to get a list of all registered facets via DiamondLoupe vm.expectRevert(DiamondIsPaused.selector); - DiamondLoupeFacet(address(emergencyPauseFacet)).facets(); + DiamondLoupeFacet(address(emergencyPauseFacetExtended)).facets(); } function test_UnauthorizedWalletCannotPauseDiamond() public { vm.startPrank(USER_SENDER); vm.expectRevert(UnAuthorized.selector); // pause the contract - emergencyPauseFacet.pauseDiamond(); + emergencyPauseFacetExtended.pauseDiamond(); vm.startPrank(USER_RECEIVER); vm.expectRevert(UnAuthorized.selector); // pause the contract - emergencyPauseFacet.pauseDiamond(); + emergencyPauseFacetExtended.pauseDiamond(); } function test_DiamondOwnerCanUnpauseDiamond() public { IDiamondLoupe.Facet[] memory initialFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // pause diamond first @@ -129,14 +147,20 @@ contract EmergencyPauseFacetPRODTest is TestBase { // unpause diamond as owner vm.startPrank(USER_DIAMOND_OWNER_MAINNET); - vm.expectEmit(true, true, true, true, address(emergencyPauseFacet)); + vm.expectEmit( + true, + true, + true, + true, + address(emergencyPauseFacetExtended) + ); emit EmergencyUnpaused(USER_DIAMOND_OWNER_MAINNET); - emergencyPauseFacet.unpauseDiamond(blacklist); + emergencyPauseFacetExtended.unpauseDiamond(blacklist); // make sure diamond works normal again and has all facets reinstated IDiamondLoupe.Facet[] memory finalFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); assertTrue(initialFacets.length == finalFacets.length); @@ -149,26 +173,27 @@ contract EmergencyPauseFacetPRODTest is TestBase { // try to pause the diamond with various wallets vm.startPrank(USER_PAUSER); vm.expectRevert(OnlyContractOwner.selector); - emergencyPauseFacet.unpauseDiamond(blacklist); + emergencyPauseFacetExtended.unpauseDiamond(blacklist); vm.startPrank(USER_RECEIVER); vm.expectRevert(OnlyContractOwner.selector); - emergencyPauseFacet.unpauseDiamond(blacklist); + emergencyPauseFacetExtended.unpauseDiamond(blacklist); // make sure diamond is still paused vm.expectRevert(DiamondIsPaused.selector); - DiamondLoupeFacet(address(emergencyPauseFacet)).facets(); + DiamondLoupeFacet(address(emergencyPauseFacetExtended)).facets(); } function test_DiamondOwnerCanRemoveFacet() public { // get a list of all registered facet addresses IDiamondLoupe.Facet[] memory initialFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // get PeripheryRegistryFacet address - address facetAddress = DiamondLoupeFacet(address(emergencyPauseFacet)) - .facetAddress( + address facetAddress = DiamondLoupeFacet( + address(emergencyPauseFacetExtended) + ).facetAddress( PeripheryRegistryFacet(address(emergencyPauseFacet)) .registerPeripheryContract .selector @@ -177,25 +202,32 @@ contract EmergencyPauseFacetPRODTest is TestBase { // remove facet vm.startPrank(USER_DIAMOND_OWNER_MAINNET); - vm.expectEmit(true, true, true, true, address(emergencyPauseFacet)); + vm.expectEmit( + true, + true, + true, + true, + address(emergencyPauseFacetExtended) + ); emit EmergencyFacetRemoved(facetAddress, USER_DIAMOND_OWNER_MAINNET); - emergencyPauseFacet.removeFacet(facetAddress); + emergencyPauseFacetExtended.removeFacet(facetAddress); // get a list of all registered facet addresses IDiamondLoupe.Facet[] memory finalFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // ensure that one facet less is registered now assertTrue(initialFacets.length == finalFacets.length + 1); // ensure that PeripheryRegistryFacet function selector is not associated to any facetAddress assertTrue( - DiamondLoupeFacet(address(emergencyPauseFacet)).facetAddress( - PeripheryRegistryFacet(address(emergencyPauseFacet)) - .registerPeripheryContract - .selector - ) == address(0) + DiamondLoupeFacet(address(emergencyPauseFacetExtended)) + .facetAddress( + PeripheryRegistryFacet(address(emergencyPauseFacet)) + .registerPeripheryContract + .selector + ) == address(0) ); vm.stopPrank(); @@ -204,13 +236,14 @@ contract EmergencyPauseFacetPRODTest is TestBase { function test_PauserWalletCanRemoveFacet() public { // get a list of all registered facet addresses IDiamondLoupe.Facet[] memory initialFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // get PeripheryRegistryFacet address - address facetAddress = DiamondLoupeFacet(address(emergencyPauseFacet)) - .facetAddress( - PeripheryRegistryFacet(address(emergencyPauseFacet)) + address facetAddress = DiamondLoupeFacet( + address(emergencyPauseFacetExtended) + ).facetAddress( + PeripheryRegistryFacet(address(emergencyPauseFacetExtended)) .registerPeripheryContract .selector ); @@ -218,25 +251,32 @@ contract EmergencyPauseFacetPRODTest is TestBase { // remove facet vm.startPrank(USER_PAUSER); - vm.expectEmit(true, true, true, true, address(emergencyPauseFacet)); + vm.expectEmit( + true, + true, + true, + true, + address(emergencyPauseFacetExtended) + ); emit EmergencyFacetRemoved(facetAddress, USER_PAUSER); - emergencyPauseFacet.removeFacet(facetAddress); + emergencyPauseFacetExtended.removeFacet(facetAddress); // get a list of all registered facet addresses IDiamondLoupe.Facet[] memory finalFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // ensure that one facet less is registered now assertTrue(initialFacets.length == finalFacets.length + 1); // ensure that PeripheryRegistryFacet function selector is not associated to any facetAddress assertTrue( - DiamondLoupeFacet(address(emergencyPauseFacet)).facetAddress( - PeripheryRegistryFacet(address(emergencyPauseFacet)) - .registerPeripheryContract - .selector - ) == address(0) + DiamondLoupeFacet(address(emergencyPauseFacetExtended)) + .facetAddress( + PeripheryRegistryFacet( + address(emergencyPauseFacetExtended) + ).registerPeripheryContract.selector + ) == address(0) ); vm.stopPrank(); @@ -245,13 +285,14 @@ contract EmergencyPauseFacetPRODTest is TestBase { function test_UnauthorizedWalletCannotRemoveFacet() public { // get a list of all registered facet addresses IDiamondLoupe.Facet[] memory initialFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // get PeripheryRegistryFacet address - address facetAddress = DiamondLoupeFacet(address(emergencyPauseFacet)) - .facetAddress( - PeripheryRegistryFacet(address(emergencyPauseFacet)) + address facetAddress = DiamondLoupeFacet( + address(emergencyPauseFacetExtended) + ).facetAddress( + PeripheryRegistryFacet(address(emergencyPauseFacetExtended)) .registerPeripheryContract .selector ); @@ -259,15 +300,15 @@ contract EmergencyPauseFacetPRODTest is TestBase { // try to remove facet vm.startPrank(USER_SENDER); vm.expectRevert(UnAuthorized.selector); - emergencyPauseFacet.removeFacet(facetAddress); + emergencyPauseFacetExtended.removeFacet(facetAddress); vm.startPrank(USER_RECEIVER); vm.expectRevert(UnAuthorized.selector); - emergencyPauseFacet.removeFacet(facetAddress); + emergencyPauseFacetExtended.removeFacet(facetAddress); // get a list of all registered facet addresses IDiamondLoupe.Facet[] memory finalFacets = DiamondLoupeFacet( - address(emergencyPauseFacet) + address(emergencyPauseFacetExtended) ).facets(); // ensure that number of facets remains unchanged diff --git a/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.local.t.sol b/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.local.t.sol index 429a62107..99dd5f793 100644 --- a/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.local.t.sol +++ b/test/solidity/Facets/EmergencyPauseFacet/EmergencyPauseFacet.local.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { TestBase } from "../../utils/TestBase.sol"; -import { OnlyContractOwner, InvalidCallData, UnAuthorized, DiamondIsPaused, FunctionDoesNotExist } from "src/Errors/GenericErrors.sol"; +import { OnlyContractOwner, InvalidCallData, UnAuthorized, DiamondIsPaused, FunctionDoesNotExist, InvalidConfig } from "lifi/Errors/GenericErrors.sol"; import { EmergencyPauseFacet } from "lifi/Facets/EmergencyPauseFacet.sol"; import { PeripheryRegistryFacet } from "lifi/Facets/PeripheryRegistryFacet.sol"; import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; @@ -23,23 +23,17 @@ contract EmergencyPauseFacetLOCALTest is TestBase { error NoFacetToPause(); uint256 internal counter; - event DiamondCut( - LibDiamond.FacetCut[] _diamondCut, - address _init, - bytes _calldata - ); // STORAGE - EmergencyPauseFacet internal emergencyPauseFacet; address[] internal blacklist = new address[](0); - function setUp() public { + function setUp() public override { // set custom block number for forking customBlockNumberForForking = 19979843; initTestBase(); - // // no need to add the facet to the diamond, it's already added in DiamondTest.sol + // no need to add the facet to the diamond, it's already added in DiamondTest.sol emergencyPauseFacet = EmergencyPauseFacet(payable(address(diamond))); // set facet address in TestBase @@ -49,9 +43,13 @@ contract EmergencyPauseFacetLOCALTest is TestBase { ); } + function testRevert_WhenPauserWalletIsZeroAddress() public { + vm.expectRevert(InvalidConfig.selector); + new EmergencyPauseFacet(address(0)); + } + function test_PauserWalletCanPauseDiamond() public { vm.startPrank(USER_PAUSER); - vm.expectEmit(true, true, true, true, address(emergencyPauseFacet)); emit EmergencyPaused(USER_PAUSER); @@ -144,13 +142,12 @@ contract EmergencyPauseFacetLOCALTest is TestBase { } function test_CanUnpauseDiamondWithSingleBlacklist() public { - address ownershipFacetAddress = 0xB021CCbe1bd1EF2af8221A79E89dD3145947A082; + address ownershipFacetAddress = address(ownershipFacet); // get function selectors of OwnershipFacet bytes4[] memory ownershipFunctionSelectors = IDiamondLoupe( address(diamond) ).facetFunctionSelectors(ownershipFacetAddress); - // pause diamond first test_PauserWalletCanPauseDiamond(); @@ -200,8 +197,8 @@ contract EmergencyPauseFacetLOCALTest is TestBase { emit EmergencyUnpaused(USER_DIAMOND_OWNER); blacklist = new address[](2); - blacklist[0] = 0xB021CCbe1bd1EF2af8221A79E89dD3145947A082; // OwnershipFacet - blacklist[1] = 0xA412555Fa40F6AA4B67a773dB5a7f85983890341; // PeripheryRegistryFacet + blacklist[0] = address(ownershipFacet); // OwnershipFacet + blacklist[1] = address(peripheryFacet); // PeripheryRegistryFacet emergencyPauseFacet.unpauseDiamond(blacklist); diff --git a/test/solidity/Facets/GardenFacet.t.sol b/test/solidity/Facets/GardenFacet.t.sol index 3714a18c4..15c8bb69f 100644 --- a/test/solidity/Facets/GardenFacet.t.sol +++ b/test/solidity/Facets/GardenFacet.t.sol @@ -34,7 +34,7 @@ contract GardenFacetTest is TestBaseFacet { ILiFi.BridgeData internal validBridgeData; GardenFacet.GardenData internal validGardenData; - function setUp() public { + function setUp() public virtual override { customBlockNumberForForking = 23346384; initTestBase(); @@ -46,7 +46,7 @@ contract GardenFacetTest is TestBaseFacet { .selector; functionSelectors[2] = gardenFacet.addAllowedContractSelector.selector; - addFacet(diamond, address(gardenFacet), functionSelectors); + addFacet(address(diamond), address(gardenFacet), functionSelectors); gardenFacet = TestGardenFacet(address(diamond)); diff --git a/test/solidity/Facets/GasZipFacet.t.sol b/test/solidity/Facets/GasZipFacet.t.sol index e91465ec0..159f7215c 100644 --- a/test/solidity/Facets/GasZipFacet.t.sol +++ b/test/solidity/Facets/GasZipFacet.t.sol @@ -33,7 +33,7 @@ contract GasZipFacetTest is TestBaseFacet { error OnlyNativeAllowed(); error TooManyChainIds(); - function setUp() public { + function setUp() public override { // set custom block no for mainnet forking customBlockNumberForForking = 20828620; @@ -50,13 +50,16 @@ contract GasZipFacetTest is TestBaseFacet { .selector; functionSelectors[2] = gasZipFacet.getDestinationChainsValue.selector; - functionSelectors[3] = gasZipFacet.addAllowedContractSelector.selector; - addFacet(diamond, address(gasZipFacet), functionSelectors); + functionSelectors[3] = gasZipFacet.addAllowedContractSelector.selector; + addFacet(address(diamond), address(gasZipFacet), functionSelectors); gasZipFacet = TestGasZipFacet(payable(address(diamond))); // whitelist uniswap dex with function selectors - gasZipFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); + gasZipFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); gasZipFacet.addAllowedContractSelector( address(uniswap), uniswap.swapTokensForExactETH.selector @@ -69,7 +72,10 @@ contract GasZipFacetTest is TestBaseFacet { address(uniswap), uniswap.swapETHForExactTokens.selector ); - gasZipFacet.addAllowedContractSelector(address(gasZipFacet), uniswap.swapExactTokensForTokens.selector); + gasZipFacet.addAllowedContractSelector( + address(gasZipFacet), + uniswap.swapExactTokensForTokens.selector + ); gasZipFacet.addAllowedContractSelector( address(gasZipFacet), uniswap.swapTokensForExactETH.selector diff --git a/test/solidity/Facets/GenericSwapFacet.t.sol b/test/solidity/Facets/GenericSwapFacet.t.sol index 8f7d8c14e..eade05ca3 100644 --- a/test/solidity/Facets/GenericSwapFacet.t.sol +++ b/test/solidity/Facets/GenericSwapFacet.t.sol @@ -19,7 +19,7 @@ contract GenericSwapFacetTest is TestBase { TestGenericSwapFacet internal genericSwapFacet; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 15588208; initTestBase(); @@ -28,12 +28,21 @@ contract GenericSwapFacetTest is TestBase { bytes4[] memory functionSelectors = new bytes4[](3); functionSelectors[0] = genericSwapFacet.swapTokensGeneric.selector; - functionSelectors[1] = genericSwapFacet.addAllowedContractSelector.selector; + functionSelectors[1] = genericSwapFacet + .addAllowedContractSelector + .selector; - addFacet(diamond, address(genericSwapFacet), functionSelectors); + addFacet( + address(diamond), + address(genericSwapFacet), + functionSelectors + ); genericSwapFacet = TestGenericSwapFacet(address(diamond)); - genericSwapFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); + genericSwapFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); // set facet address in TestBase setFacetAddressInTestBase( diff --git a/test/solidity/Facets/GenericSwapFacetV3.t.sol b/test/solidity/Facets/GenericSwapFacetV3.t.sol index 103fddb03..3dbec274f 100644 --- a/test/solidity/Facets/GenericSwapFacetV3.t.sol +++ b/test/solidity/Facets/GenericSwapFacetV3.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import { GenericSwapFacet } from "lifi/Facets/GenericSwapFacet.sol"; import { GenericSwapFacetV3 } from "lifi/Facets/GenericSwapFacetV3.sol"; import { ContractCallNotAllowed, CumulativeSlippageTooHigh, NativeAssetTransferFailed } from "lifi/Errors/GenericErrors.sol"; -import { TestHelpers, MockUniswapDEX, NonETHReceiver } from "../utils/TestHelpers.sol"; +import { MockUniswapDEX, NonETHReceiver } from "../utils/TestHelpers.sol"; import { ERC20, SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; import { LibSwap, TestBase } from "../utils/TestBase.sol"; import { TestWhitelistManagerBase } from "../utils/TestWhitelistManagerBase.sol"; @@ -20,7 +20,7 @@ contract TestGenericSwapFacetV3 is contract TestGenericSwapFacet is GenericSwapFacet, TestWhitelistManagerBase {} -contract GenericSwapFacetV3Test is TestBase, TestHelpers { +contract GenericSwapFacetV3Test is TestBase { using SafeTransferLib for ERC20; // These values are for Mainnet @@ -38,7 +38,7 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { TestGenericSwapFacet internal genericSwapFacet; TestGenericSwapFacetV3 internal genericSwapFacetV3; - function setUp() public { + function setUp() public override { // set custom block number for forking customBlockNumberForForking = 19834820; initTestBase(); @@ -49,9 +49,17 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { // add genericSwapFacet (v1) to diamond (for gas usage comparison) bytes4[] memory functionSelectors = new bytes4[](3); functionSelectors[0] = genericSwapFacet.swapTokensGeneric.selector; - functionSelectors[1] = genericSwapFacet.addAllowedContractSelector.selector; - functionSelectors[2] = genericSwapFacet.removeAllowedContractSelector.selector; - addFacet(diamond, address(genericSwapFacet), functionSelectors); + functionSelectors[1] = genericSwapFacet + .addAllowedContractSelector + .selector; + functionSelectors[2] = genericSwapFacet + .removeAllowedContractSelector + .selector; + addFacet( + address(diamond), + address(genericSwapFacet), + functionSelectors + ); // add genericSwapFacet (v3) to diamond bytes4[] memory functionSelectorsV3 = new bytes4[](6); @@ -74,15 +82,25 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { .swapTokensMultipleV3NativeToERC20 .selector; - addFacet(diamond, address(genericSwapFacetV3), functionSelectorsV3); + addFacet( + address(diamond), + address(genericSwapFacetV3), + functionSelectorsV3 + ); genericSwapFacet = TestGenericSwapFacet(address(diamond)); genericSwapFacetV3 = TestGenericSwapFacetV3(address(diamond)); // whitelist uniswap dex with function selectors // v1 - genericSwapFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - genericSwapFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); + genericSwapFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + genericSwapFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); genericSwapFacet.addAllowedContractSelector( address(uniswap), uniswap.swapExactTokensForETH.selector @@ -92,21 +110,51 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { uniswap.swapExactETHForTokens.selector ); // v3 - genericSwapFacetV3.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - genericSwapFacetV3.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); - genericSwapFacetV3.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForETH.selector); - genericSwapFacetV3.addAllowedContractSelector(address(uniswap), uniswap.swapExactETHForTokens.selector); + genericSwapFacetV3.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + genericSwapFacetV3.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); + genericSwapFacetV3.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForETH.selector + ); + genericSwapFacetV3.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactETHForTokens.selector + ); // whitelist feeCollector with function selectors // v1 - genericSwapFacet.addAllowedContractSelector(FEE_COLLECTOR, feeCollector.collectTokenFees.selector); - genericSwapFacet.addAllowedContractSelector(FEE_COLLECTOR, feeCollector.collectNativeFees.selector); + genericSwapFacet.addAllowedContractSelector( + FEE_COLLECTOR, + feeCollector.collectTokenFees.selector + ); + genericSwapFacet.addAllowedContractSelector( + FEE_COLLECTOR, + feeCollector.collectNativeFees.selector + ); // v3 - genericSwapFacetV3.addAllowedContractSelector(FEE_COLLECTOR, feeCollector.collectTokenFees.selector); - genericSwapFacetV3.addAllowedContractSelector(FEE_COLLECTOR, feeCollector.collectNativeFees.selector); + genericSwapFacetV3.addAllowedContractSelector( + FEE_COLLECTOR, + feeCollector.collectTokenFees.selector + ); + genericSwapFacetV3.addAllowedContractSelector( + FEE_COLLECTOR, + feeCollector.collectNativeFees.selector + ); // v3 - genericSwapFacetV3.addAllowedContractSelector(FEE_COLLECTOR, feeCollector.collectTokenFees.selector); - genericSwapFacetV3.addAllowedContractSelector(FEE_COLLECTOR, feeCollector.collectNativeFees.selector); + genericSwapFacetV3.addAllowedContractSelector( + FEE_COLLECTOR, + feeCollector.collectTokenFees.selector + ); + genericSwapFacetV3.addAllowedContractSelector( + FEE_COLLECTOR, + feeCollector.collectNativeFees.selector + ); // set facet address in TestBase setFacetAddressInTestBase( @@ -627,7 +675,10 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { vm.startPrank(USDC_HOLDER); // remove dex from whitelist - genericSwapFacetV3.removeAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); + genericSwapFacetV3.removeAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); vm.expectRevert(ContractCallNotAllowed.selector); @@ -783,7 +834,10 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { ) = _produceSwapDataNativeToERC20(); // remove dex from whitelist - genericSwapFacetV3.removeAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactETHForTokens.selector); + genericSwapFacetV3.removeAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactETHForTokens.selector + ); vm.expectRevert(ContractCallNotAllowed.selector); @@ -1091,7 +1145,10 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { ); // remove dex from whitelist - genericSwapFacetV3.removeAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); + genericSwapFacetV3.removeAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); vm.expectRevert(ContractCallNotAllowed.selector); @@ -1924,7 +1981,10 @@ contract GenericSwapFacetV3Test is TestBase, TestHelpers { ); // whitelist DEX & function selector - genericSwapFacet.addAllowedContractSelector(address(mockDex), mockDex.swapTokensForExactTokens.selector); + genericSwapFacet.addAllowedContractSelector( + address(mockDex), + mockDex.swapTokensForExactTokens.selector + ); usdc.approve(address(genericSwapFacet), amountIn); diff --git a/test/solidity/Facets/GlacisFacet.t.sol b/test/solidity/Facets/GlacisFacet.t.sol index 3444bb758..0f978fa26 100644 --- a/test/solidity/Facets/GlacisFacet.t.sol +++ b/test/solidity/Facets/GlacisFacet.t.sol @@ -27,7 +27,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { uint256 internal payableAmount = 1 ether; - function setUp() public virtual { + function setUp() public virtual override { initTestBase(); srcToken = ERC20(addressSrcToken); @@ -51,7 +51,7 @@ abstract contract GlacisFacetTestBase is TestBaseFacet { .removeAllowedContractSelector .selector; - addFacet(diamond, address(glacisFacet), functionSelectors); + addFacet(address(diamond), address(glacisFacet), functionSelectors); glacisFacet = TestGlacisFacet(address(diamond)); glacisFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/GnosisBridgeFacet.t.sol b/test/solidity/Facets/GnosisBridgeFacet.t.sol index 8eb0ecb9e..50c340521 100644 --- a/test/solidity/Facets/GnosisBridgeFacet.t.sol +++ b/test/solidity/Facets/GnosisBridgeFacet.t.sol @@ -29,7 +29,7 @@ contract GnosisBridgeFacetTest is TestBaseFacet { TestGnosisBridgeFacet internal gnosisBridgeFacet; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22566858; initTestBase(); defaultUSDSAmount = defaultDAIAmount; @@ -49,19 +49,37 @@ contract GnosisBridgeFacetTest is TestBaseFacet { functionSelectors[1] = gnosisBridgeFacet .swapAndStartBridgeTokensViaGnosisBridge .selector; - functionSelectors[2] = gnosisBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = gnosisBridgeFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = gnosisBridgeFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(gnosisBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(gnosisBridgeFacet), + functionSelectors + ); gnosisBridgeFacet = TestGnosisBridgeFacet(address(diamond)); - gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForETH.selector); - gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); - gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); + gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForETH.selector + ); + gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); + gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); setFacetAddressInTestBase(address(gnosisBridgeFacet), "GnosisFacet"); diff --git a/test/solidity/Facets/HopFacet.t.sol b/test/solidity/Facets/HopFacet.t.sol index 6fc2b0146..4f98b4127 100644 --- a/test/solidity/Facets/HopFacet.t.sol +++ b/test/solidity/Facets/HopFacet.t.sol @@ -5,8 +5,8 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { LibSwap, TestBaseFacet } from "../utils/TestBaseFacet.sol"; import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; import { HopFacet } from "lifi/Facets/HopFacet.sol"; +import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; import { OnlyContractOwner, InvalidConfig, InvalidAmount } from "src/Errors/GenericErrors.sol"; -import { LiFiDiamond } from "../utils/DiamondTest.sol"; import { TestWhitelistManagerBase } from "../utils/TestWhitelistManagerBase.sol"; // Stub HopFacet Contract @@ -31,7 +31,7 @@ contract HopFacetTest is TestBaseFacet { ILiFi.BridgeData internal validBridgeData; HopFacet.HopData internal validHopData; - function setUp() public { + function setUp() public override { initTestBase(); hopFacet = new TestHopFacet(); bytes4[] memory functionSelectors = new bytes4[](6); @@ -42,11 +42,9 @@ contract HopFacetTest is TestBaseFacet { functionSelectors[2] = hopFacet.initHop.selector; functionSelectors[3] = hopFacet.registerBridge.selector; functionSelectors[4] = hopFacet.addAllowedContractSelector.selector; - functionSelectors[5] = hopFacet - .removeAllowedContractSelector - .selector; + functionSelectors[5] = hopFacet.removeAllowedContractSelector.selector; - addFacet(diamond, address(hopFacet), functionSelectors); + addFacet(address(diamond), address(hopFacet), functionSelectors); HopFacet.Config[] memory configs = new HopFacet.Config[](3); configs[0] = HopFacet.Config(ADDRESS_USDC, USDC_BRIDGE); @@ -56,10 +54,22 @@ contract HopFacetTest is TestBaseFacet { hopFacet = TestHopFacet(address(diamond)); hopFacet.initHop(configs); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactETHForTokens.selector); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactETHForTokens.selector + ); setFacetAddressInTestBase(address(hopFacet), "HopFacet"); @@ -234,7 +244,7 @@ contract HopFacetTest is TestBaseFacet { .removeAllowedContractSelector .selector; - addFacet(diamond2, address(hopFacet2), functionSelectors); + addFacet(address(diamond2), address(hopFacet2), functionSelectors); HopFacet.Config[] memory configs = new HopFacet.Config[](3); configs[0] = HopFacet.Config(ADDRESS_USDC, USDC_BRIDGE); @@ -269,7 +279,7 @@ contract HopFacetTest is TestBaseFacet { functionSelectors[2] = hopFacet2.initHop.selector; functionSelectors[3] = hopFacet2.registerBridge.selector; - addFacet(diamond, address(hopFacet2), functionSelectors); + addFacet(address(diamond), address(hopFacet2), functionSelectors); HopFacet.Config[] memory configs = new HopFacet.Config[](1); configs[0] = HopFacet.Config(addressUSDCPolygon, ammWrapperPolygon); diff --git a/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL1.t.sol b/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL1.t.sol index 81d78cee8..2e8a4eefd 100644 --- a/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL1.t.sol +++ b/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL1.t.sol @@ -26,7 +26,7 @@ contract HopFacetOptimizedL1Test is TestBaseFacet { ILiFi.BridgeData internal validBridgeData; HopFacetOptimized.HopData internal validHopData; - function setUp() public { + function setUp() public override { initTestBase(); hopFacet = new TestHopFacet(); bytes4[] memory functionSelectors = new bytes4[](7); @@ -44,20 +44,33 @@ contract HopFacetOptimizedL1Test is TestBaseFacet { .selector; functionSelectors[4] = hopFacet.setApprovalForBridges.selector; functionSelectors[5] = hopFacet.addAllowedContractSelector.selector; - functionSelectors[6] = hopFacet - .removeAllowedContractSelector - .selector; + functionSelectors[6] = hopFacet.removeAllowedContractSelector.selector; - addFacet(diamond, address(hopFacet), functionSelectors); + addFacet(address(diamond), address(hopFacet), functionSelectors); hopFacet = TestHopFacet(address(diamond)); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactETHForTokens.selector); - + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactETHForTokens.selector + ); + setFacetAddressInTestBase(address(hopFacet), "HopFacet"); // Set approval for all bridges diff --git a/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL2.t.sol b/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL2.t.sol index 9a5c08590..d2e7b10aa 100644 --- a/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL2.t.sol +++ b/test/solidity/Facets/HopFacetOptimized/HopFacetOptimizedL2.t.sol @@ -26,7 +26,7 @@ contract HopFacetOptimizedL2Test is TestBaseFacet { ILiFi.BridgeData internal validBridgeData; HopFacetOptimized.HopData internal validHopData; - function setUp() public { + function setUp() public override { // Custom Config customRpcUrlForForking = "ETH_NODE_URI_POLYGON"; customBlockNumberForForking = 59534582; @@ -48,19 +48,32 @@ contract HopFacetOptimizedL2Test is TestBaseFacet { .selector; functionSelectors[4] = hopFacet.setApprovalForBridges.selector; functionSelectors[5] = hopFacet.addAllowedContractSelector.selector; - functionSelectors[6] = hopFacet - .removeAllowedContractSelector - .selector; + functionSelectors[6] = hopFacet.removeAllowedContractSelector.selector; - addFacet(diamond, address(hopFacet), functionSelectors); + addFacet(address(diamond), address(hopFacet), functionSelectors); hopFacet = TestHopFacet(address(diamond)); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactETHForTokens.selector); - hopFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactETHForTokens.selector); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactETHForTokens.selector + ); + hopFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactETHForTokens.selector + ); setFacetAddressInTestBase(address(hopFacet), "HopFacet"); diff --git a/test/solidity/Facets/HopFacetPacked/HopFacetPackedL1.t.sol b/test/solidity/Facets/HopFacetPacked/HopFacetPackedL1.t.sol index 5c2840fe2..d1b9c98bd 100644 --- a/test/solidity/Facets/HopFacetPacked/HopFacetPackedL1.t.sol +++ b/test/solidity/Facets/HopFacetPacked/HopFacetPackedL1.t.sol @@ -59,7 +59,7 @@ contract HopFacetPackedL1Test is TestBase { BridgeParams internal usdtParams; BridgeParams internal nativeParams; - function setUp() public { + function setUp() public override { // set custom block number for forking customBlockNumberForForking = 15588208; initTestBase(); @@ -113,7 +113,7 @@ contract HopFacetPackedL1Test is TestBase { .encode_startBridgeTokensViaHopL1ERC20Packed .selector; - addFacet(diamond, address(hopFacetPacked), functionSelectors); + addFacet(address(diamond), address(hopFacetPacked), functionSelectors); hopFacetPacked = HopFacetPacked(address(diamond)); /// Approval @@ -131,7 +131,7 @@ contract HopFacetPackedL1Test is TestBase { .setApprovalForBridges .selector; addFacet( - diamond, + address(diamond), address(hopFacetOptimized), functionSelectorsApproval ); diff --git a/test/solidity/Facets/HopFacetPacked/HopFacetPackedL2.t.sol b/test/solidity/Facets/HopFacetPacked/HopFacetPackedL2.t.sol index d3e30cae7..ef76b4dc7 100644 --- a/test/solidity/Facets/HopFacetPacked/HopFacetPackedL2.t.sol +++ b/test/solidity/Facets/HopFacetPacked/HopFacetPackedL2.t.sol @@ -69,7 +69,7 @@ contract HopFacetPackedL2Test is TestBase { BridgeParams internal usdcBridgeParams; BridgeParams internal usdtBridgeParams; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 58467500; customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; initTestBase(); @@ -120,7 +120,7 @@ contract HopFacetPackedL2Test is TestBase { .encode_startBridgeTokensViaHopL1ERC20Packed .selector; - addFacet(diamond, address(hopFacetPacked), functionSelectors); + addFacet(address(diamond), address(hopFacetPacked), functionSelectors); hopFacetPacked = HopFacetPacked(address(diamond)); /// Approval @@ -150,7 +150,7 @@ contract HopFacetPackedL2Test is TestBase { .setApprovalForBridges .selector; addFacet( - diamond, + address(diamond), address(hopFacetOptimized), functionSelectorsApproval ); diff --git a/test/solidity/Facets/MayanFacet.t.sol b/test/solidity/Facets/MayanFacet.t.sol index 3ab3398fc..8c3a68465 100644 --- a/test/solidity/Facets/MayanFacet.t.sol +++ b/test/solidity/Facets/MayanFacet.t.sol @@ -54,7 +54,7 @@ contract MayanFacetTest is TestBaseFacet { error InvalidReceiver(address expected, address actual); error ProtocolDataTooShort(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19968172; initTestBase(); @@ -103,7 +103,11 @@ contract MayanFacetTest is TestBaseFacet { .removeAllowedContractSelector .selector; - addFacet(diamond, address(mayanBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(mayanBridgeFacet), + functionSelectors + ); mayanBridgeFacet = TestMayanFacet(address(diamond)); mayanBridgeFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/OmniBridgeFacet/OmniBridgeFacet.t.sol b/test/solidity/Facets/OmniBridgeFacet/OmniBridgeFacet.t.sol index 8d1c7374d..cd1e29c8e 100644 --- a/test/solidity/Facets/OmniBridgeFacet/OmniBridgeFacet.t.sol +++ b/test/solidity/Facets/OmniBridgeFacet/OmniBridgeFacet.t.sol @@ -25,7 +25,7 @@ contract OmniBridgeFacetTest is TestBaseFacet { TestOmniBridgeFacet internal omniBridgeFacet; - function setUp() public { + function setUp() public override { initTestBase(); omniBridgeFacet = new TestOmniBridgeFacet( @@ -40,18 +40,33 @@ contract OmniBridgeFacetTest is TestBaseFacet { functionSelectors[1] = omniBridgeFacet .swapAndStartBridgeTokensViaOmniBridge .selector; - functionSelectors[2] = omniBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = omniBridgeFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = omniBridgeFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(omniBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(omniBridgeFacet), + functionSelectors + ); omniBridgeFacet = TestOmniBridgeFacet(address(diamond)); - omniBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - omniBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - omniBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); + omniBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + omniBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + omniBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); setFacetAddressInTestBase(address(omniBridgeFacet), "OmniBridgeFacet"); diff --git a/test/solidity/Facets/OmniBridgeFacet/OmniBridgeL2Facet.t.sol b/test/solidity/Facets/OmniBridgeFacet/OmniBridgeL2Facet.t.sol index 1d7db3fbb..952b632b4 100644 --- a/test/solidity/Facets/OmniBridgeFacet/OmniBridgeL2Facet.t.sol +++ b/test/solidity/Facets/OmniBridgeFacet/OmniBridgeL2Facet.t.sol @@ -25,7 +25,7 @@ contract OmniBridgeL2FacetTest is TestBaseFacet { TestOmniBridgeFacet internal omniBridgeFacet; - function setUp() public { + function setUp() public override { // Fork Gnosis chain customRpcUrlForForking = "ETH_NODE_URI_GNOSIS"; customBlockNumberForForking = 26862566; @@ -49,18 +49,33 @@ contract OmniBridgeL2FacetTest is TestBaseFacet { functionSelectors[1] = omniBridgeFacet .swapAndStartBridgeTokensViaOmniBridge .selector; - functionSelectors[2] = omniBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = omniBridgeFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = omniBridgeFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(omniBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(omniBridgeFacet), + functionSelectors + ); omniBridgeFacet = TestOmniBridgeFacet(address(diamond)); - omniBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - omniBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - omniBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); + omniBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + omniBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + omniBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); setFacetAddressInTestBase(address(omniBridgeFacet), "OmniBridgeFacet"); diff --git a/test/solidity/Facets/OptimismBridgeFacet.t.sol b/test/solidity/Facets/OptimismBridgeFacet.t.sol index 31555149d..6ad697281 100644 --- a/test/solidity/Facets/OptimismBridgeFacet.t.sol +++ b/test/solidity/Facets/OptimismBridgeFacet.t.sol @@ -40,7 +40,7 @@ contract OptimismBridgeFacetTest is TestBase { ILiFi.BridgeData internal validBridgeData; OptimismBridgeFacet.OptimismData internal validOptimismData; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 15876510; initTestBase(); @@ -54,12 +54,18 @@ contract OptimismBridgeFacetTest is TestBase { .swapAndStartBridgeTokensViaOptimismBridge .selector; functionSelectors[2] = optimismBridgeFacet.initOptimism.selector; - functionSelectors[3] = optimismBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[3] = optimismBridgeFacet + .addAllowedContractSelector + .selector; functionSelectors[4] = optimismBridgeFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(optimismBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(optimismBridgeFacet), + functionSelectors + ); OptimismBridgeFacet.Config[] memory configs = new OptimismBridgeFacet.Config[](1); @@ -71,9 +77,18 @@ contract OptimismBridgeFacetTest is TestBase { IL1StandardBridge(STANDARD_BRIDGE) ); - optimismBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - optimismBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - optimismBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); + optimismBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + optimismBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + optimismBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); validBridgeData = ILiFi.BridgeData({ transactionId: "", diff --git a/test/solidity/Facets/OwnershipFacet.t.sol b/test/solidity/Facets/OwnershipFacet.t.sol index 4c7996cf1..5637ed2b3 100644 --- a/test/solidity/Facets/OwnershipFacet.t.sol +++ b/test/solidity/Facets/OwnershipFacet.t.sol @@ -6,8 +6,6 @@ import { TestBase } from "../utils/TestBase.sol"; import { OnlyContractOwner } from "lifi/Errors/GenericErrors.sol"; contract OwnershipFacetTest is TestBase { - OwnershipFacet internal ownershipFacet; - error NoNullOwner(); error NewOwnerMustNotBeSelf(); error NoPendingOwnershipTransfer(); @@ -18,12 +16,7 @@ contract OwnershipFacetTest is TestBase { address indexed _to ); - event OwnershipTransferred( - address indexed previousOwner, - address indexed newOwner - ); - - function setUp() public { + function setUp() public override { initTestBase(); ownershipFacet = OwnershipFacet(address(diamond)); diff --git a/test/solidity/Facets/PioneerFacet.t.sol b/test/solidity/Facets/PioneerFacet.t.sol index f6d28b45d..c9e84e069 100644 --- a/test/solidity/Facets/PioneerFacet.t.sol +++ b/test/solidity/Facets/PioneerFacet.t.sol @@ -23,7 +23,7 @@ contract PioneerFacetTest is TestBaseFacet { PioneerFacet.PioneerData internal pioneerData; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 17130542; initTestBase(); @@ -41,7 +41,11 @@ contract PioneerFacetTest is TestBaseFacet { .addAllowedContractSelector .selector; - addFacet(diamond, address(basePioneerFacet), functionSelectors); + addFacet( + address(diamond), + address(basePioneerFacet), + functionSelectors + ); pioneerFacet = TestPioneerFacet(address(diamond)); pioneerFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/PolygonBridgeFacet.t.sol b/test/solidity/Facets/PolygonBridgeFacet.t.sol index b98642645..ffa68c716 100644 --- a/test/solidity/Facets/PolygonBridgeFacet.t.sol +++ b/test/solidity/Facets/PolygonBridgeFacet.t.sol @@ -28,7 +28,7 @@ contract PolygonBridgeFacetTest is TestBaseFacet { TestPolygonBridgeFacet internal polygonBridgeFacet; - function setUp() public { + function setUp() public override { initTestBase(); polygonBridgeFacet = new TestPolygonBridgeFacet( @@ -43,17 +43,29 @@ contract PolygonBridgeFacetTest is TestBaseFacet { functionSelectors[1] = polygonBridgeFacet .swapAndStartBridgeTokensViaPolygonBridge .selector; - functionSelectors[2] = polygonBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = polygonBridgeFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = polygonBridgeFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(polygonBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(polygonBridgeFacet), + functionSelectors + ); polygonBridgeFacet = TestPolygonBridgeFacet(address(diamond)); - polygonBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - polygonBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); + polygonBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + polygonBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); setFacetAddressInTestBase( address(polygonBridgeFacet), diff --git a/test/solidity/Facets/RelayDepositoryFacet.t.sol b/test/solidity/Facets/RelayDepositoryFacet.t.sol index 314b3c5cf..b53791e9d 100644 --- a/test/solidity/Facets/RelayDepositoryFacet.t.sol +++ b/test/solidity/Facets/RelayDepositoryFacet.t.sol @@ -122,7 +122,7 @@ contract RelayDepositoryFacetTest is TestBaseFacet { address internal constant ALLOCATOR_ADDRESS = 0x1234567890123456789012345678901234567890; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19767662; initTestBase(); @@ -145,7 +145,11 @@ contract RelayDepositoryFacetTest is TestBaseFacet { .addAllowedContractSelector .selector; - addFacet(diamond, address(relayDepositoryFacet), functionSelectors); + addFacet( + address(diamond), + address(relayDepositoryFacet), + functionSelectors + ); relayDepositoryFacet = TestRelayDepositoryFacet(address(diamond)); // Setup DEX approvals diff --git a/test/solidity/Facets/RelayFacet.t.sol b/test/solidity/Facets/RelayFacet.t.sol index a7caaedc2..5c38d1506 100644 --- a/test/solidity/Facets/RelayFacet.t.sol +++ b/test/solidity/Facets/RelayFacet.t.sol @@ -43,7 +43,7 @@ contract RelayFacetTest is TestBaseFacet { error InvalidQuote(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19767662; initTestBase(); relayFacet = new TestRelayFacet(RELAY_RECEIVER, relaySolver); @@ -59,7 +59,7 @@ contract RelayFacetTest is TestBaseFacet { functionSelectors[4] = relayFacet.getMappedChainId.selector; functionSelectors[5] = relayFacet.setConsumedId.selector; - addFacet(diamond, address(relayFacet), functionSelectors); + addFacet(address(diamond), address(relayFacet), functionSelectors); relayFacet = TestRelayFacet(address(diamond)); relayFacet.addAllowedContractSelector( ADDRESS_UNISWAP, diff --git a/test/solidity/Facets/SquidFacet.t.sol b/test/solidity/Facets/SquidFacet.t.sol index 117993075..7cd0e6c67 100644 --- a/test/solidity/Facets/SquidFacet.t.sol +++ b/test/solidity/Facets/SquidFacet.t.sol @@ -34,7 +34,7 @@ contract SquidFacetTest is TestBaseFacet { ISquidMulticall.Call internal sourceCall; TestSquidFacet internal squidFacet; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 18810880; customBlockNumberForForking = 19664946; initTestBase(); @@ -50,12 +50,24 @@ contract SquidFacetTest is TestBaseFacet { .removeAllowedContractSelector .selector; - addFacet(diamond, address(squidFacet), functionSelectors); + addFacet(address(diamond), address(squidFacet), functionSelectors); squidFacet = TestSquidFacet(address(diamond)); - squidFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - squidFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); - squidFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); - squidFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForETH.selector); + squidFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + squidFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); + squidFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); + squidFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForETH.selector + ); setFacetAddressInTestBase(address(squidFacet), "SquidFacet"); diff --git a/test/solidity/Facets/StargateFacetV2.t.sol b/test/solidity/Facets/StargateFacetV2.t.sol index 22f620f1d..357e076d9 100644 --- a/test/solidity/Facets/StargateFacetV2.t.sol +++ b/test/solidity/Facets/StargateFacetV2.t.sol @@ -44,7 +44,7 @@ contract StargateFacetV2Test is TestBaseFacet { TestStargateFacetV2 internal stargateFacetV2; StargateFacetV2.StargateData internal stargateData; - function setUp() public { + function setUp() public override { // set custom block number for forking customBlockNumberForForking = 19979843; @@ -61,22 +61,46 @@ contract StargateFacetV2Test is TestBaseFacet { functionSelectors[1] = stargateFacetV2 .swapAndStartBridgeTokensViaStargate .selector; - functionSelectors[2] = stargateFacetV2.addAllowedContractSelector.selector; + functionSelectors[2] = stargateFacetV2 + .addAllowedContractSelector + .selector; functionSelectors[3] = stargateFacetV2 .removeAllowedContractSelector .selector; functionSelectors[4] = stargateFacetV2.tokenMessaging.selector; - addFacet(diamond, address(stargateFacetV2), functionSelectors); + addFacet( + address(diamond), + address(stargateFacetV2), + functionSelectors + ); stargateFacetV2 = TestStargateFacetV2(payable(address(diamond))); // whitelist DEX and feeCollector addresses and function selectors in diamond - stargateFacetV2.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - stargateFacetV2.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - stargateFacetV2.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForETH.selector); - stargateFacetV2.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); - stargateFacetV2.addAllowedContractSelector(address(feeCollector), feeCollector.collectNativeFees.selector); - stargateFacetV2.addAllowedContractSelector(address(feeCollector), feeCollector.collectTokenFees.selector); + stargateFacetV2.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + stargateFacetV2.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + stargateFacetV2.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForETH.selector + ); + stargateFacetV2.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); + stargateFacetV2.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectNativeFees.selector + ); + stargateFacetV2.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectTokenFees.selector + ); // set facet address in TestBase setFacetAddressInTestBase(address(stargateFacetV2), "StargateFacetV2"); diff --git a/test/solidity/Facets/SymbiosisFacet.t.sol b/test/solidity/Facets/SymbiosisFacet.t.sol index 477fbb3c3..d2a0dbcb5 100644 --- a/test/solidity/Facets/SymbiosisFacet.t.sol +++ b/test/solidity/Facets/SymbiosisFacet.t.sol @@ -26,7 +26,7 @@ contract SymbiosisFacetTest is TestBaseFacet { TestSymbiosisFacet internal symbiosisFacet; SymbiosisFacet.SymbiosisData internal symbiosisData; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 19317492; initTestBase(); // MockMetaRouter mockMetaRouter = new MockMetaRouter(); @@ -44,19 +44,33 @@ contract SymbiosisFacetTest is TestBaseFacet { functionSelectors[1] = symbiosisFacet .swapAndStartBridgeTokensViaSymbiosis .selector; - functionSelectors[2] = symbiosisFacet.addAllowedContractSelector.selector; + functionSelectors[2] = symbiosisFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = symbiosisFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(symbiosisFacet), functionSelectors); + addFacet(address(diamond), address(symbiosisFacet), functionSelectors); symbiosisFacet = TestSymbiosisFacet(address(diamond)); - symbiosisFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - symbiosisFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForETH.selector); - symbiosisFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - symbiosisFacet.addAllowedContractSelector(address(uniswap), uniswap.swapTokensForExactETH.selector); + symbiosisFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + symbiosisFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForETH.selector + ); + symbiosisFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + symbiosisFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapTokensForExactETH.selector + ); setFacetAddressInTestBase(address(symbiosisFacet), "SymbiosisFacet"); diff --git a/test/solidity/Facets/ThorSwapFacet.t.sol b/test/solidity/Facets/ThorSwapFacet.t.sol index 41d640774..c6bafdf14 100644 --- a/test/solidity/Facets/ThorSwapFacet.t.sol +++ b/test/solidity/Facets/ThorSwapFacet.t.sol @@ -18,7 +18,7 @@ contract ThorSwapFacetTest is TestBaseFacet { ThorSwapFacet.ThorSwapData internal validThorSwapData; TestThorSwapFacet internal thorSwapFacet; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 16661275; initTestBase(); @@ -30,18 +30,29 @@ contract ThorSwapFacetTest is TestBaseFacet { functionSelectors[1] = thorSwapFacet .swapAndStartBridgeTokensViaThorSwap .selector; - functionSelectors[2] = thorSwapFacet.addAllowedContractSelector.selector; + functionSelectors[2] = thorSwapFacet + .addAllowedContractSelector + .selector; functionSelectors[3] = thorSwapFacet .removeAllowedContractSelector .selector; - addFacet(diamond, address(thorSwapFacet), functionSelectors); + addFacet(address(diamond), address(thorSwapFacet), functionSelectors); thorSwapFacet = TestThorSwapFacet(address(diamond)); - thorSwapFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapExactTokensForTokens.selector); - thorSwapFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapTokensForExactETH.selector); - thorSwapFacet.addAllowedContractSelector(ADDRESS_UNISWAP, uniswap.swapETHForExactTokens.selector); - + thorSwapFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapExactTokensForTokens.selector + ); + thorSwapFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapTokensForExactETH.selector + ); + thorSwapFacet.addAllowedContractSelector( + ADDRESS_UNISWAP, + uniswap.swapETHForExactTokens.selector + ); + setFacetAddressInTestBase(address(thorSwapFacet), "ThorSwapFacet"); // adjust bridgeData diff --git a/test/solidity/Facets/UnitFacet.t.sol b/test/solidity/Facets/UnitFacet.t.sol index 9e911ff54..7276173cd 100644 --- a/test/solidity/Facets/UnitFacet.t.sol +++ b/test/solidity/Facets/UnitFacet.t.sol @@ -53,7 +53,7 @@ contract UnitFacetTest is TestBaseFacet { error NotSupported(); - function setUp() public { + function setUp() public virtual override { customBlockNumberForForking = 17130542; initTestBase(); @@ -65,7 +65,7 @@ contract UnitFacetTest is TestBaseFacet { .selector; functionSelectors[2] = unitFacet.addAllowedContractSelector.selector; - addFacet(diamond, address(unitFacet), functionSelectors); + addFacet(address(diamond), address(unitFacet), functionSelectors); unitFacet = TestUnitFacet(address(diamond)); // whitelist uniswap dex with function selectors unitFacet.addAllowedContractSelector( diff --git a/test/solidity/Facets/WhitelistManagerFacet.t.sol b/test/solidity/Facets/WhitelistManagerFacet.t.sol index c454032d6..69adee76d 100644 --- a/test/solidity/Facets/WhitelistManagerFacet.t.sol +++ b/test/solidity/Facets/WhitelistManagerFacet.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; -import { DSTest } from "ds-test/test.sol"; -import { DiamondTest, LiFiDiamond } from "../utils/DiamondTest.sol"; import { WhitelistManagerFacet } from "lifi/Facets/WhitelistManagerFacet.sol"; import { AccessManagerFacet } from "lifi/Facets/AccessManagerFacet.sol"; import { OwnershipFacet } from "src/Facets/OwnershipFacet.sol"; @@ -12,6 +10,7 @@ import { TestBase } from "../utils/TestBase.sol"; import { stdJson } from "forge-std/StdJson.sol"; import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; +import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; import { DeployScript } from "../../../script/deploy/facets/UpdateWhitelistManagerFacet.s.sol"; contract Foo {} @@ -41,12 +40,10 @@ contract MockSwapperFacet { } } -contract WhitelistManagerFacetTest is DSTest, DiamondTest { - address internal constant USER_PAUSER = address(0xdeadbeef); - address internal constant USER_DIAMOND_OWNER = address(0x123456); +// contract WhitelistManagerFacetTest is DSTest, DiamondTest { +contract WhitelistManagerFacetTest is TestBase { address internal constant NOT_DIAMOND_OWNER = address(0xabc123456); - LiFiDiamond internal diamond; WhitelistManagerFacet internal whitelistMgr; AccessManagerFacet internal accessMgr; Foo internal c1; @@ -61,8 +58,8 @@ contract WhitelistManagerFacetTest is DSTest, DiamondTest { bool indexed approved ); - function setUp() public { - diamond = createDiamond(USER_DIAMOND_OWNER, USER_PAUSER); + function setUp() public virtual override { + initTestBase(); whitelistMgr = new WhitelistManagerFacet(); c1 = new Foo(); c2 = new Foo(); @@ -99,7 +96,7 @@ contract WhitelistManagerFacetTest is DSTest, DiamondTest { .getAllContractSelectorPairs .selector; - addFacet(diamond, address(whitelistMgr), functionSelectors); + addFacet(address(diamond), address(whitelistMgr), functionSelectors); // add AccessManagerFacet to be able to whitelist addresses for execution of protected functions accessMgr = new AccessManagerFacet(); @@ -107,7 +104,7 @@ contract WhitelistManagerFacetTest is DSTest, DiamondTest { functionSelectors = new bytes4[](2); functionSelectors[0] = accessMgr.setCanExecute.selector; functionSelectors[1] = accessMgr.addressCanExecuteMethod.selector; - addFacet(diamond, address(accessMgr), functionSelectors); + addFacet(address(diamond), address(accessMgr), functionSelectors); accessMgr = AccessManagerFacet(address(diamond)); whitelistMgr = WhitelistManagerFacet(address(diamond)); @@ -1221,11 +1218,15 @@ contract WhitelistManagerFacetMigrationTest is TestBase { ExposedUpdateWhitelistManagerFacetDeployScript internal deployScript; address[] internal oldContractsBeforeMigration; - function setUp() public { + function setUp() public virtual override { // fork mainnet to test with real production state string memory rpcUrl = vm.envString("ETH_NODE_URI_BASE"); vm.createSelectFork(rpcUrl, 33206380); + // Initialize diamond to point to the forked diamond address + // This is needed for inherited tests like test_DeploysWithoutErrors() + diamond = LiFiDiamond(payable(DIAMOND)); + // Set required environment variables for deployment script vm.setEnv("NETWORK", "base"); vm.setEnv("FILE_SUFFIX", ""); @@ -1240,6 +1241,28 @@ contract WhitelistManagerFacetMigrationTest is TestBase { deployScript = new ExposedUpdateWhitelistManagerFacetDeployScript(); } + /// @notice Override to return the forked diamond address + function getDiamondAddress() internal pure override returns (address) { + return DIAMOND; + } + + /// @notice Override to return the actual owner of the forked diamond + function getDiamondOwner() internal view override returns (address) { + return OwnershipFacet(DIAMOND).owner(); + } + + /// @notice Override test to use forked diamond instead of creating a new one + function test_DeploysWithoutErrors() public override { + assertTrue( + getDiamondAddress() != address(0), + "Diamond should be deployed" + ); + assertTrue( + getDiamondAddress() == DIAMOND, + "Diamond should be the forked diamond address" + ); + } + /// @notice Test that non-owner cannot call migrate function function testRevert_FailsIfNonOwnerTriesToCallMigrate() public { // Deploy WhitelistManagerFacet first @@ -1469,7 +1492,7 @@ contract WhitelistManagerFacetMigrationTest is TestBase { .isSelectorAllowedLegacy .selector; addFacet( - LiFiDiamond(payable(DIAMOND)), + address(diamond), address(mockSwapperFacet), mockSwapperSelectors ); @@ -1677,7 +1700,7 @@ contract WhitelistManagerFacetMigrationTest is TestBase { .isContractSelectorAllowed .selector; addFacet( - LiFiDiamond(payable(DIAMOND)), + address(diamond), address(mockSwapperFacet), mockSwapperSelectors ); diff --git a/test/solidity/Gas/CBridgeFacetPackedARB.gas.t.sol b/test/solidity/Gas/CBridgeFacetPackedARB.gas.t.sol index d492404c7..9dd7c3302 100644 --- a/test/solidity/Gas/CBridgeFacetPackedARB.gas.t.sol +++ b/test/solidity/Gas/CBridgeFacetPackedARB.gas.t.sol @@ -27,7 +27,7 @@ contract CBridgeGasARBTest is TestBase { uint256 internal amountUSDC; bytes internal packedUSDC; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 58467500; customRpcUrlForForking = "ETH_NODE_URI_ARBITRUM"; initTestBase(); @@ -57,7 +57,11 @@ contract CBridgeGasARBTest is TestBase { .encode_startBridgeTokensViaCBridgeERC20Packed .selector; - addFacet(diamond, address(cBridgeFacetPacked), functionSelectors); + addFacet( + address(diamond), + address(cBridgeFacetPacked), + functionSelectors + ); cBridgeFacetPacked = CBridgeFacetPacked(payable(address(diamond))); /// Perpare parameters diff --git a/test/solidity/Gas/CBridgeFacetPackedETH.gas.t.sol b/test/solidity/Gas/CBridgeFacetPackedETH.gas.t.sol index 43810c59f..efb3f6458 100644 --- a/test/solidity/Gas/CBridgeFacetPackedETH.gas.t.sol +++ b/test/solidity/Gas/CBridgeFacetPackedETH.gas.t.sol @@ -41,7 +41,7 @@ contract CBridgeGasETHTest is TestBase { ILiFi.BridgeData internal bridgeDataUSDC; CBridgeFacet.CBridgeData internal cbridgeDataUSDC; - function setUp() public { + function setUp() public override { customBlockNumberForForking = 15588208; initTestBase(); @@ -71,7 +71,11 @@ contract CBridgeGasETHTest is TestBase { .encode_startBridgeTokensViaCBridgeERC20Packed .selector; - addFacet(diamond, address(cBridgeFacetPacked), functionSelectors); + addFacet( + address(diamond), + address(cBridgeFacetPacked), + functionSelectors + ); cBridgeFacetPacked = CBridgeFacetPacked(payable(address(diamond))); /// Perpare CBridgeFacet @@ -82,7 +86,7 @@ contract CBridgeGasETHTest is TestBase { .startBridgeTokensViaCBridge .selector; - addFacet(diamond, address(cBridgeFacet), functionSelectors2); + addFacet(address(diamond), address(cBridgeFacet), functionSelectors2); cBridgeFacet = CBridgeFacet(address(diamond)); /// Perpare parameters @@ -170,7 +174,7 @@ contract CBridgeGasETHTest is TestBase { .setApprovalForBridges .selector; addFacet( - diamond, + address(diamond), address(hopFacetOptimized), functionSelectorsApproval ); diff --git a/test/solidity/Gas/Hop.t.sol b/test/solidity/Gas/Hop.t.sol index 5785539ef..1139b75e9 100644 --- a/test/solidity/Gas/Hop.t.sol +++ b/test/solidity/Gas/Hop.t.sol @@ -13,7 +13,7 @@ contract HopGasTest is TestBase { IHopBridge internal hop; HopFacet internal hopFacet; - function setUp() public { + function setUp() public override { // set custom block number for forking customBlockNumberForForking = 14847528; initTestBase(); @@ -25,7 +25,7 @@ contract HopGasTest is TestBase { functionSelectors[0] = hopFacet.initHop.selector; functionSelectors[1] = hopFacet.startBridgeTokensViaHop.selector; - addFacet(diamond, address(hopFacet), functionSelectors); + addFacet(address(diamond), address(hopFacet), functionSelectors); hopFacet = HopFacet(address(diamond)); HopFacet.Config[] memory config = new HopFacet.Config[](1); diff --git a/test/solidity/Gas/HopFacetPackedARB.gas.t.sol b/test/solidity/Gas/HopFacetPackedARB.gas.t.sol index e590bbc04..8a2ed50bd 100644 --- a/test/solidity/Gas/HopFacetPackedARB.gas.t.sol +++ b/test/solidity/Gas/HopFacetPackedARB.gas.t.sol @@ -98,7 +98,7 @@ pragma solidity ^0.8.17; // .encoder_startBridgeTokensViaHopL2ERC20Packed // .selector; -// addFacet(diamond, address(hopFacetPacked), functionSelectors); +// addFacet(address(diamond), address(hopFacetPacked), functionSelectors); // hopFacetPacked = HopFacetPacked(address(diamond)); // /// Perpare HopFacetOptimized & Approval diff --git a/test/solidity/Gas/HopFacetPackedETH.gas.t.sol b/test/solidity/Gas/HopFacetPackedETH.gas.t.sol index 38c992f81..e707c644b 100644 --- a/test/solidity/Gas/HopFacetPackedETH.gas.t.sol +++ b/test/solidity/Gas/HopFacetPackedETH.gas.t.sol @@ -70,7 +70,7 @@ pragma solidity ^0.8.17; // .startBridgeTokensViaHopL1ERC20Min // .selector; -// addFacet(diamond, address(hopFacetPacked), functionSelectors); +// addFacet(address(diamond), address(hopFacetPacked), functionSelectors); // hopFacetPacked = HopFacetPacked(address(diamond)); // /// Perpare HopFacetOptimized & Approval diff --git a/test/solidity/Gas/HopFacetPackedPOL.gas.t.sol b/test/solidity/Gas/HopFacetPackedPOL.gas.t.sol index daefa092a..8f0e894ee 100644 --- a/test/solidity/Gas/HopFacetPackedPOL.gas.t.sol +++ b/test/solidity/Gas/HopFacetPackedPOL.gas.t.sol @@ -99,7 +99,7 @@ pragma solidity ^0.8.17; // .encoder_startBridgeTokensViaHopL2ERC20Packed // .selector; -// addFacet(diamond, address(hopFacetPacked), functionSelectors); +// addFacet(address(diamond), address(hopFacetPacked), functionSelectors); // hopFacetPacked = HopFacetPacked(address(diamond)); // /// Perpare HopFacetOptimized & Approval diff --git a/test/solidity/Helpers/SwapperV2.t.sol b/test/solidity/Helpers/SwapperV2.t.sol index b78f921d3..0a36090db 100644 --- a/test/solidity/Helpers/SwapperV2.t.sol +++ b/test/solidity/Helpers/SwapperV2.t.sol @@ -73,7 +73,7 @@ contract TestSwapperV2 is SwapperV2, TestWhitelistManagerBase { contract SwapperV2Test is TestBase { TestAMM internal amm; TestSwapperV2 internal swapper; - function setUp() public { + function setUp() public override { initTestBase(); amm = new TestAMM(); @@ -90,11 +90,14 @@ contract SwapperV2Test is TestBase { .removeAllowedContractSelector .selector; - addFacet(diamond, address(swapper), functionSelectors); + addFacet(address(diamond), address(swapper), functionSelectors); swapper = TestSwapperV2(address(diamond)); swapper.addAllowedContractSelector(address(amm), amm.swap.selector); - swapper.addAllowedContractSelector(address(amm), amm.partialSwap.selector); + swapper.addAllowedContractSelector( + address(amm), + amm.partialSwap.selector + ); } function test_SwapCleanup() public { diff --git a/test/solidity/Helpers/WithdrawablePeriphery.t.sol b/test/solidity/Helpers/WithdrawablePeriphery.t.sol index 08596cd67..881e0fc41 100644 --- a/test/solidity/Helpers/WithdrawablePeriphery.t.sol +++ b/test/solidity/Helpers/WithdrawablePeriphery.t.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: LGPL-3.0-only -/// @custom:version 1.0.0 pragma solidity ^0.8.17; import { WithdrawablePeriphery } from "lifi/Helpers/WithdrawablePeriphery.sol"; @@ -22,7 +21,7 @@ contract WithdrawablePeripheryTest is TestBase { error UnAuthorized(); - function setUp() public { + function setUp() public override { initTestBase(); // deploy contract diff --git a/test/solidity/LiFiDiamond.t.sol b/test/solidity/LiFiDiamond.t.sol index 13414fb89..37dd18fa7 100644 --- a/test/solidity/LiFiDiamond.t.sol +++ b/test/solidity/LiFiDiamond.t.sol @@ -1,108 +1,35 @@ -// SPDX-License-Identifier: Unlicensed +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; -import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; -import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; -import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; -import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; -import { DSTest } from "ds-test/test.sol"; -import { Vm } from "forge-std/Vm.sol"; - -contract LiFiDiamondTest is DSTest { - // solhint-disable immutable-vars-naming - Vm internal immutable vm = Vm(HEVM_ADDRESS); - LiFiDiamond public diamond; - DiamondCutFacet public diamondCutFacet; - OwnershipFacet public ownershipFacet; - address public diamondOwner; - - event DiamondCut( - LibDiamond.FacetCut[] _diamondCut, - address _init, - bytes _calldata - ); - - event OwnershipTransferred( - address indexed previousOwner, - address indexed newOwner - ); - - error FunctionDoesNotExist(); - error ShouldNotReachThisCode(); - error InvalidDiamondSetup(); - error ExternalCallFailed(); - - function setUp() public { - diamondOwner = address(123456); - diamondCutFacet = new DiamondCutFacet(); - ownershipFacet = new OwnershipFacet(); - - // prepare function selector for diamondCut (OwnershipFacet) - bytes4[] memory functionSelectors = new bytes4[](1); - functionSelectors[0] = ownershipFacet.owner.selector; - - // prepare parameters for diamondCut (OwnershipFacet) - LibDiamond.FacetCut[] memory cut = new LibDiamond.FacetCut[](1); - cut[0] = LibDiamond.FacetCut({ - facetAddress: address(ownershipFacet), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: functionSelectors - }); - - diamond = new LiFiDiamond(diamondOwner, address(diamondCutFacet)); +import { BaseDiamondTest } from "./utils/BaseDiamondTest.sol"; +import { InvalidConfig } from "lifi/Errors/GenericErrors.sol"; + +contract LiFiDiamondTest is BaseDiamondTest { + function setUp() public virtual override { + super.setUp(); + // Call createDiamond to get a fully configured diamond with all facets + createDiamond(USER_DIAMOND_OWNER, USER_PAUSER); } - function test_DeploysWithoutErrors() public { - diamond = new LiFiDiamond(diamondOwner, address(diamondCutFacet)); + /// @notice Test that LiFiDiamond can be deployed without errors + function test_DeploysWithoutErrors() public override { + LiFiDiamond testDiamond = new LiFiDiamond( + USER_DIAMOND_OWNER, + address(diamondCutFacet) + ); + assertTrue( + address(testDiamond) != address(0), + "Diamond should be deployed" + ); } - function test_ForwardsCallsViaDelegateCall() public { - // only one facet with one selector is registered (diamondCut) - vm.startPrank(diamondOwner); - - DiamondLoupeFacet diamondLoupe = new DiamondLoupeFacet(); - - // make sure that this call fails (without ending the test) - bool failed = false; - try DiamondLoupeFacet(address(diamond)).facetAddresses() returns ( - address[] memory - ) {} catch { - failed = true; - } - if (!failed) revert InvalidDiamondSetup(); - - // prepare function selectors - bytes4[] memory functionSelectors = new bytes4[](4); - functionSelectors[0] = diamondLoupe.facets.selector; - functionSelectors[1] = diamondLoupe.facetFunctionSelectors.selector; - functionSelectors[2] = diamondLoupe.facetAddresses.selector; - functionSelectors[3] = diamondLoupe.facetAddress.selector; - - // prepare diamondCut - LibDiamond.FacetCut[] memory cuts = new LibDiamond.FacetCut[](1); - cuts[0] = LibDiamond.FacetCut({ - facetAddress: address(diamondLoupe), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: functionSelectors - }); - - DiamondCutFacet(address(diamond)).diamondCut(cuts, address(0), ""); - } - - function test_RevertsOnUnknownFunctionSelector() public { - // call random function selectors - bytes memory callData = hex"a516f0f3"; // getPeripheryContract(string) - - vm.expectRevert(FunctionDoesNotExist.selector); - (bool success, ) = address(diamond).call(callData); - if (!success) revert ShouldNotReachThisCode(); // was only added to silence a compiler warning - } - - function test_CanReceiveETH() public { - (bool success, ) = address(diamond).call{ value: 1 ether }(""); - if (!success) revert ExternalCallFailed(); - - assertEq(address(diamond).balance, 1 ether); + /// @notice Test that LiFiDiamond reverts when constructed with zero address owner + function testRevert_LiFiDiamondConstructedWithZeroAddressOwner() public { + vm.expectRevert(InvalidConfig.selector); + new LiFiDiamond( + address(0), // This should trigger InvalidConfig + address(diamondCutFacet) + ); } } diff --git a/test/solidity/Libraries/LibAsset.t.sol b/test/solidity/Libraries/LibAsset.t.sol index b5985ebc6..3dd57b5da 100644 --- a/test/solidity/Libraries/LibAsset.t.sol +++ b/test/solidity/Libraries/LibAsset.t.sol @@ -50,7 +50,7 @@ contract LibAssetImplementer { contract LibAssetTest is TestBase { LibAssetImplementer internal implementer; - function setUp() public { + function setUp() public override { implementer = new LibAssetImplementer(); initTestBase(); } diff --git a/test/solidity/Periphery/Across/V3/ReceiverAcrossV3.t.sol b/test/solidity/Periphery/Across/V3/ReceiverAcrossV3.t.sol index 441f2fc0b..231f0f6c2 100644 --- a/test/solidity/Periphery/Across/V3/ReceiverAcrossV3.t.sol +++ b/test/solidity/Periphery/Across/V3/ReceiverAcrossV3.t.sol @@ -25,7 +25,7 @@ contract ReceiverAcrossV3Test is TestBase { event ExecutorSet(address indexed executor); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 20024274; initTestBase(); diff --git a/test/solidity/Periphery/Across/V4/ReceiverAcrossV4.t.sol b/test/solidity/Periphery/Across/V4/ReceiverAcrossV4.t.sol index 7a4997380..18737b950 100644 --- a/test/solidity/Periphery/Across/V4/ReceiverAcrossV4.t.sol +++ b/test/solidity/Periphery/Across/V4/ReceiverAcrossV4.t.sol @@ -25,7 +25,7 @@ contract ReceiverAcrossV4Test is TestBase { event ExecutorSet(address indexed executor); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22989702; initTestBase(); diff --git a/test/solidity/Periphery/FeeForwarder.t.sol b/test/solidity/Periphery/FeeForwarder.t.sol index 9f250b9c9..b9e098685 100644 --- a/test/solidity/Periphery/FeeForwarder.t.sol +++ b/test/solidity/Periphery/FeeForwarder.t.sol @@ -27,7 +27,7 @@ contract FeeForwarderTest is TestBase { FeeForwarder.FeeDistribution[] distributions ); - function setUp() public { + function setUp() public virtual override { // Initialize TestBase which sets up USER_SENDER, WITHDRAW_WALLET, etc. initTestBase(); diff --git a/test/solidity/Periphery/GasZipPeriphery.t.sol b/test/solidity/Periphery/GasZipPeriphery.t.sol index 973c3af36..eeaf59d33 100644 --- a/test/solidity/Periphery/GasZipPeriphery.t.sol +++ b/test/solidity/Periphery/GasZipPeriphery.t.sol @@ -1,16 +1,19 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; import { GasZipPeriphery } from "lifi/Periphery/GasZipPeriphery.sol"; -import { LiFiDEXAggregator } from "lifi/Periphery/LiFiDEXAggregator.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { TestGnosisBridgeFacet } from "test/solidity/Facets/GnosisBridgeFacet.t.sol"; -import { TestBase, ILiFi } from "../utils/TestBase.sol"; import { IGnosisBridgeRouter } from "lifi/Interfaces/IGnosisBridgeRouter.sol"; import { IGasZip } from "lifi/Interfaces/IGasZip.sol"; -import { NonETHReceiver } from "../utils/TestHelpers.sol"; import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { CoreRouteFacet } from "lifi/Periphery/LDA/Facets/CoreRouteFacet.sol"; +import { UniV2StyleFacet } from "lifi/Periphery/LDA/Facets/UniV2StyleFacet.sol"; +import { NativeWrapperFacet } from "lifi/Periphery/LDA/Facets/NativeWrapperFacet.sol"; +import { TestBase, ILiFi } from "../utils/TestBase.sol"; +import { NonETHReceiver } from "../utils/TestHelpers.sol"; import { TestWhitelistManagerBase } from "../utils/TestWhitelistManagerBase.sol"; +import { LiFiDEXAggregatorDiamondTest } from "../Periphery/LDA/LiFiDEXAggregatorDiamond.t.sol"; // Stub GenericSwapFacet Contract contract TestGasZipPeriphery is GasZipPeriphery, TestWhitelistManagerBase { @@ -26,12 +29,12 @@ contract GasZipPeripheryTest is TestBase { 0x2a37D63EAdFe4b4682a3c28C1c2cD4F109Cc2762; address internal constant GNOSIS_BRIDGE_ROUTER = 0x9a873656c19Efecbfb4f9FAb5B7acdeAb466a0B0; + address internal constant UNIV2_PAIR_DAI_WETH = + 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; TestGnosisBridgeFacet internal gnosisBridgeFacet; TestGasZipPeriphery internal gasZipPeriphery; IGasZip.GasZipData internal defaultGasZipData; - LiFiDEXAggregator internal liFiDEXAggregator; - address[] internal privileged; bytes32 internal defaultReceiverBytes32 = bytes32(uint256(uint160(USER_RECEIVER))); uint256 internal defaultNativeDepositAmount = 1e16; @@ -43,30 +46,22 @@ contract GasZipPeripheryTest is TestBase { error TooManyChainIds(); error ETHTransferFailed(); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 22566858; initTestBase(); - - privileged = new address[](2); - privileged[0] = address(0xABC); - privileged[1] = address(0xEBC); - - liFiDEXAggregator = new LiFiDEXAggregator( - address(0xCAFE), - privileged, - USER_DIAMOND_OWNER - ); + LiFiDEXAggregatorDiamondTest.setUp(); // deploy contracts gasZipPeriphery = new TestGasZipPeriphery( GAS_ZIP_ROUTER_MAINNET, - address(liFiDEXAggregator), + address(ldaDiamond), USER_DIAMOND_OWNER ); defaultUSDCAmount = 10 * 10 ** usdc.decimals(); // 10 USDC // set up diamond with GnosisBridgeFacet so we have a bridge to test with gnosisBridgeFacet = _getGnosisBridgeFacet(); + _wireLDARouteFacets(); defaultGasZipData = IGasZip.GasZipData({ receiverAddress: defaultReceiverBytes32, @@ -79,13 +74,13 @@ contract GasZipPeripheryTest is TestBase { bridgeData.destinationChainId = 100; vm.label(address(gasZipPeriphery), "GasZipPeriphery"); - vm.label(address(liFiDEXAggregator), "LiFiDEXAggregator"); + vm.label(address(ldaDiamond), "LiFiDEXAggregator"); } function test_WillStoreConstructorParametersCorrectly() public { gasZipPeriphery = new TestGasZipPeriphery( GAS_ZIP_ROUTER_MAINNET, - address(liFiDEXAggregator), + address(ldaDiamond), USER_DIAMOND_OWNER ); @@ -93,10 +88,7 @@ contract GasZipPeripheryTest is TestBase { address(gasZipPeriphery.gasZipRouter()), GAS_ZIP_ROUTER_MAINNET ); - assertEq( - gasZipPeriphery.liFiDEXAggregator(), - address(liFiDEXAggregator) - ); + assertEq(gasZipPeriphery.liFiDEXAggregator(), address(ldaDiamond)); } function test_CanDepositNative() public { @@ -216,13 +208,15 @@ contract GasZipPeripheryTest is TestBase { // // get swapData for gas zip uint256 gasZipERC20Amount = 2 * 10 ** dai.decimals(); - ( - LibSwap.SwapData memory gasZipSwapData, - - ) = _getLiFiDEXAggregatorCalldataForERC20ToNativeSwap( - address(liFiDEXAggregator), + LibSwap.SwapData + memory gasZipSwapData = _getLiFiDEXAggregatorCalldataForERC20ToNativeSwap( ADDRESS_DAI, - gasZipERC20Amount + gasZipERC20Amount, + 0, // minAmountOut + UNIV2_PAIR_DAI_WETH, + true, + 3000, + ADDRESS_WRAPPED_NATIVE ); swapData[2] = LibSwap.SwapData( @@ -254,8 +248,14 @@ contract GasZipPeripheryTest is TestBase { }); // whitelist gasZipPeriphery and FeeCollector - gasZipPeriphery.addAllowedContractSelector(address(gasZipPeriphery), gasZipPeriphery.depositToGasZipERC20.selector); - gasZipPeriphery.addAllowedContractSelector(address(feeCollector), feeCollector.collectTokenFees.selector); + gasZipPeriphery.addAllowedContractSelector( + address(gasZipPeriphery), + gasZipPeriphery.depositToGasZipERC20.selector + ); + gasZipPeriphery.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectTokenFees.selector + ); // set approval for bridging usdc.approve(address(gnosisBridgeFacet), defaultUSDCAmount); @@ -340,7 +340,10 @@ contract GasZipPeripheryTest is TestBase { }); // whitelist gasZipPeriphery and FeeCollector - gasZipPeriphery.addAllowedContractSelector(address(gasZipPeriphery), gasZipPeriphery.depositToGasZipNative.selector); + gasZipPeriphery.addAllowedContractSelector( + address(gasZipPeriphery), + gasZipPeriphery.depositToGasZipNative.selector + ); gnosisBridgeFacet.swapAndStartBridgeTokensViaGnosisBridge{ value: nativeFromAmount @@ -385,13 +388,15 @@ contract GasZipPeripheryTest is TestBase { // // get swapData for gas zip uint256 gasZipERC20Amount = 2 * 10 ** dai.decimals(); - ( - LibSwap.SwapData memory gasZipSwapData, - - ) = _getLiFiDEXAggregatorCalldataForERC20ToNativeSwap( - address(liFiDEXAggregator), + LibSwap.SwapData + memory gasZipSwapData = _getLiFiDEXAggregatorCalldataForERC20ToNativeSwap( ADDRESS_DAI, - gasZipERC20Amount + gasZipERC20Amount, + 0, // minAmountOut + UNIV2_PAIR_DAI_WETH, + true, + 3000, + ADDRESS_WRAPPED_NATIVE ); // use an invalid function selector to force the call to LiFiDEXAggregator to fail @@ -445,73 +450,209 @@ contract GasZipPeripheryTest is TestBase { functionSelectors[1] = _gnosisBridgeFacet .swapAndStartBridgeTokensViaGnosisBridge .selector; - functionSelectors[2] = _gnosisBridgeFacet.addAllowedContractSelector.selector; + functionSelectors[2] = _gnosisBridgeFacet + .addAllowedContractSelector + .selector; - addFacet(diamond, address(_gnosisBridgeFacet), functionSelectors); + addFacet( + address(diamond), + address(_gnosisBridgeFacet), + functionSelectors + ); _gnosisBridgeFacet = TestGnosisBridgeFacet(address(diamond)); // add function selectors for GasZipPeriphery - _gnosisBridgeFacet.addAllowedContractSelector(address(gasZipPeriphery), gasZipPeriphery.depositToGasZipNative.selector); - _gnosisBridgeFacet.addAllowedContractSelector(address(gasZipPeriphery), gasZipPeriphery.depositToGasZipERC20.selector); + _gnosisBridgeFacet.addAllowedContractSelector( + address(gasZipPeriphery), + gasZipPeriphery.depositToGasZipNative.selector + ); + _gnosisBridgeFacet.addAllowedContractSelector( + address(gasZipPeriphery), + gasZipPeriphery.depositToGasZipERC20.selector + ); // add function selectors for FeeCollector - _gnosisBridgeFacet.addAllowedContractSelector(address(feeCollector), feeCollector.collectTokenFees.selector); - _gnosisBridgeFacet.addAllowedContractSelector(address(feeCollector), feeCollector.collectNativeFees.selector); + _gnosisBridgeFacet.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectTokenFees.selector + ); + _gnosisBridgeFacet.addAllowedContractSelector( + address(feeCollector), + feeCollector.collectNativeFees.selector + ); // add function selectors for Uniswap - _gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForTokens.selector); - _gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactTokensForETH.selector); - _gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapETHForExactTokens.selector); - _gnosisBridgeFacet.addAllowedContractSelector(address(uniswap), uniswap.swapExactETHForTokens.selector); + _gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForTokens.selector + ); + _gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactTokensForETH.selector + ); + _gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapETHForExactTokens.selector + ); + _gnosisBridgeFacet.addAllowedContractSelector( + address(uniswap), + uniswap.swapExactETHForTokens.selector + ); setFacetAddressInTestBase(address(gnosisBridgeFacet), "GnosisFacet"); } - function _getLiFiDEXAggregatorCalldataForERC20ToNativeSwap( - address _liFiDEXAggregator, - address _sendingAssetId, - uint256 _fromAmount + function _wireLDARouteFacets() internal { + bytes4[] memory selectors; + + CoreRouteFacet core = new CoreRouteFacet(); + selectors = new bytes4[](1); + selectors[0] = CoreRouteFacet.processRoute.selector; + addFacet(address(ldaDiamond), address(core), selectors); + + UniV2StyleFacet uni = new UniV2StyleFacet(); + selectors = new bytes4[](1); + selectors[0] = UniV2StyleFacet.swapUniV2.selector; + addFacet(address(ldaDiamond), address(uni), selectors); + + NativeWrapperFacet wrap = new NativeWrapperFacet(); + selectors = new bytes4[](1); + selectors[0] = NativeWrapperFacet.unwrapNative.selector; + addFacet(address(ldaDiamond), address(wrap), selectors); + } + + // Break into smaller functions to reduce stack variables + function _buildSelectorRoute_ERC20ToNative( + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + address uniV2Pool, + bool token0ToToken1, + uint24 fee, + address wrappedNative + ) private view returns (bytes memory) { + // Build legs first + ( + bytes memory uniV2LegWithLen, + bytes memory unwrapLegWithLen + ) = _buildLegs(uniV2Pool, token0ToToken1, fee); + + // Pack route data + return + _packRouteData( + tokenIn, + amountIn, + minAmountOut, + uniV2LegWithLen, + unwrapLegWithLen, + wrappedNative + ); + } + + function _buildLegs( + address uniV2Pool, + bool token0ToToken1, + uint24 fee ) - internal + private view - returns (LibSwap.SwapData memory swapData, uint256 amountOutMin) + returns (bytes memory uniV2LegWithLen, bytes memory unwrapLegWithLen) { - // prepare swap data - address[] memory path = new address[](2); - path[0] = _sendingAssetId; - path[1] = ADDRESS_WRAPPED_NATIVE; + // Build UniV2 leg + bytes memory uniV2Payload = abi.encodePacked( + uniV2Pool, + token0ToToken1 ? uint8(1) : uint8(0), + address(ldaDiamond), + fee + ); + bytes memory uniV2Leg = abi.encodePacked( + UniV2StyleFacet.swapUniV2.selector, + uniV2Payload + ); + uniV2LegWithLen = abi.encodePacked(uint16(uniV2Leg.length), uniV2Leg); - // Calculate USDC input amount - uint256[] memory amounts = uniswap.getAmountsOut(_fromAmount, path); - amountOutMin = amounts[1] - 1; - bytes memory route = abi.encodePacked( - hex"2646478b", - //pre-commit-checker: not a secret - hex"0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", - abi.encode(_fromAmount), - //pre-commit-checker: not a secret - hex"000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - abi.encode(amountOutMin), - abi.encodePacked(hex"000000000000000000000000", gasZipPeriphery), - hex"00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000073026B175474E89094C44Da98b954EedeAC495271d0F01ffff00A478c2975Ab1Ea89e8196811F51A7B7Ade33eB1101", - _liFiDEXAggregator, - abi.encodePacked( - hex"000bb801C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc201ffff0200", - gasZipPeriphery - ), - hex"00000000000000000000000000" + // Build unwrap leg + bytes memory unwrapPayload = abi.encodePacked( + address(gasZipPeriphery) + ); + bytes memory unwrapLeg = abi.encodePacked( + NativeWrapperFacet.unwrapNative.selector, + unwrapPayload ); + unwrapLegWithLen = abi.encodePacked( + uint16(unwrapLeg.length), + unwrapLeg + ); + } - swapData = LibSwap.SwapData( - address(_liFiDEXAggregator), - address(_liFiDEXAggregator), - _sendingAssetId, - address(0), - _fromAmount, - // this is calldata for the DEXAggregator to swap 2 DAI to native + function _packRouteData( + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + bytes memory uniV2LegWithLen, + bytes memory unwrapLegWithLen, + address wrappedNative + ) private view returns (bytes memory) { + // Command 2: DistributeUserERC20 (DAI from GasZipPeriphery) → UniV2 leg sends WETH to diamond + bytes memory route = abi.encodePacked( + uint8(2), // DistributeUserERC20 + tokenIn, // token = DAI + uint8(1), // n = 1 leg + uint16(type(uint16).max), // share + uniV2LegWithLen // [len][selector|payload] + ); + // Command 1: DistributeSelfERC20 (WETH now on diamond) → unwrap to GasZipPeriphery + route = abi.encodePacked( route, - true + uint8(1), // DistributeSelfERC20 + wrappedNative, // token = WETH + uint8(1), // n = 1 leg + uint16(type(uint16).max), // share + unwrapLegWithLen // [len][selector|payload] ); + + return + abi.encodeWithSelector( + CoreRouteFacet.processRoute.selector, + tokenIn, + amountIn, + address(0), // tokenOut = native + minAmountOut, + address(gasZipPeriphery), // receiver (native) + route // bytes route + ); + } + + function _getLiFiDEXAggregatorCalldataForERC20ToNativeSwap( + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + // params you already know in your test env + address uniV2Pool, + bool token0ToToken1, + uint24 fee, + address wrappedNative + ) internal view returns (LibSwap.SwapData memory) { + bytes memory routeCall = _buildSelectorRoute_ERC20ToNative( + tokenIn, + amountIn, + minAmountOut, + uniV2Pool, + token0ToToken1, + fee, + wrappedNative + ); + + return + LibSwap.SwapData({ + callTo: address(ldaDiamond), // LiFiDEXAggregator diamond address (CoreRouteFacet lives here) + approveTo: address(ldaDiamond), + sendingAssetId: tokenIn, + receivingAssetId: address(0), // native + fromAmount: amountIn, + callData: routeCall, + requiresDeposit: true + }); } } diff --git a/test/solidity/Periphery/LDA/BaseCoreRoute.t.sol b/test/solidity/Periphery/LDA/BaseCoreRoute.t.sol new file mode 100644 index 000000000..83165a588 --- /dev/null +++ b/test/solidity/Periphery/LDA/BaseCoreRoute.t.sol @@ -0,0 +1,629 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { CoreRouteFacet } from "lifi/Periphery/LDA/Facets/CoreRouteFacet.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { TestHelpers } from "../../utils/TestHelpers.sol"; +import { LiFiDEXAggregatorDiamondTest } from "./LiFiDEXAggregatorDiamond.t.sol"; + +/// @title BaseCoreRouteTest +/// @notice Shared utilities to build route bytes and execute swaps against `CoreRouteFacet`. +/// @dev Offers: +/// - Flexible route building for single/multi-hop +/// - Event expectations helpers +/// - Overloads of `_executeAndVerifySwap` including revert path +/// Concrete tests compose these helpers to succinctly define swap scenarios. +abstract contract BaseCoreRouteTest is + LiFiDEXAggregatorDiamondTest, + TestHelpers +{ + using SafeERC20 for IERC20; + + // ==== Types ==== + + /// @notice Command types recognized by CoreRouteFacet route parser. + /// @dev Controls how `processRoute` resolves the source of funds. + enum CommandType { + None, // 0 - not used + DistributeSelfERC20, // 1 - distributeSelfERC20 (Aggregator's funds) + DistributeUserERC20, // 2 - distributeUserERC20 (User's funds) + DistributeNative, // 3 - distributeNative + DispatchSinglePoolSwap, // 4 - dispatchSinglePoolSwap (Pool's funds) + ApplyPermit // 5 - applyPermit + } + + /// @notice Generic event expectation shape for verifying external protocol emissions alongside Route. + /// @param checkTopic1 Whether to check topic1 (indexed param #1). + /// @param checkTopic2 Whether to check topic2 (indexed param #2). + /// @param checkTopic3 Whether to check topic3 (indexed param #3). + /// @param checkData Whether to check event data (non-indexed params). + /// @param eventSelector keccak256 hash of the event signature. + /// @param eventParams Params encoded as abi.encode(param) each; indexed must be exactly 32 bytes. + /// @param indexedParamIndices Indices of params that are indexed (map to topics 1..3). + struct ExpectedEvent { + bool checkTopic1; + bool checkTopic2; + bool checkTopic3; + bool checkData; + bytes32 eventSelector; // The event selector (keccak256 hash of the event signature) + bytes[] eventParams; // The event parameters, each encoded separately + uint8[] indexedParamIndices; // indices of params that are indexed (topics 1..3) + } + + /// @notice Tuning for verifying the core `Route` event. + /// @param expectedExactOut Set >0 to match exact amountOut, otherwise only structure is validated. + /// @param checkData Whether to validate event data payload. + struct RouteEventVerification { + uint256 expectedExactOut; // Only for event verification + bool checkData; + } + + /// @notice Parameters passed to `_buildBaseRoute` and `_executeAndVerifySwap`. + /// @param tokenIn Input token address (or NATIVE constant). + /// @param tokenOut Output token address (or NATIVE constant). + /// @param amountIn Input amount. + /// @param minOut Minimum acceptable output amount (slippage). + /// @param sender Logical sender of the funds for this hop. + /// @param destinationAddress Destination address of the swap proceeds. + /// @param commandType Command determining source of funds. + struct SwapTestParams { + address tokenIn; + address tokenOut; + uint256 amountIn; + uint256 minOut; + address sender; + address destinationAddress; + CommandType commandType; + } + + // ==== Constants ==== + + /// @notice Denotes 100% share in route encoding. + uint16 internal constant FULL_SHARE = 65535; + + // ==== Variables ==== + + /// @notice Proxy handle to CoreRouteFacet on the test diamond. + CoreRouteFacet internal coreRouteFacet; + + // ==== Events ==== + + /// @notice Emitted by CoreRouteFacet upon route processing completion. + /// @param from Sender address (user or synthetic if aggregator-funded). + /// @param receiverAddress Receiver address. + /// @param tokenIn Input token. + /// @param tokenOut Output token. + /// @param amountIn Input amount. + /// @param amountOutMin Min acceptable output amount. + /// @param amountOut Actual output amount. + event Route( + address indexed from, + address receiverAddress, + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOutMin, + uint256 amountOut + ); + + // ==== Errors ==== + + /// @notice Thrown if an event expectation includes >3 indexed params. + error TooManyIndexedParams(); + /// @notice Thrown when building topics from non-32-byte encoded params. + error InvalidTopicLength(); + /// @notice Thrown when data verification encounters dynamic params (unsupported). + error DynamicParamsNotSupported(); + error InvalidIndexedParamPosition(uint8 position, uint256 totalParams); + + // ==== Setup Functions ==== + + /// @notice Deploys and attaches `CoreRouteFacet` to the diamond under test. + /// @dev Invoked from `setUp` of child tests via inheritance chain. + function setUp() public virtual override { + LiFiDEXAggregatorDiamondTest.setUp(); + _addCoreRouteFacet(); + } + + /// @notice Internal helper to deploy CoreRouteFacet and add its `processRoute` selector. + /// @dev Sets `coreRouteFacet` to the diamond proxy after cut. + function _addCoreRouteFacet() internal { + coreRouteFacet = new CoreRouteFacet(); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = CoreRouteFacet.processRoute.selector; + addFacet(address(ldaDiamond), address(coreRouteFacet), selectors); + coreRouteFacet = CoreRouteFacet(payable(address(ldaDiamond))); + } + + // ==== Helper Functions ==== + + /// @notice Builds a base route-blob for a single hop given `SwapTestParams` and `swapData`. + /// @param params Swap parameters including command type. + /// @param swapData DEX-specific data (usually starts with facet.swapX.selector). + /// @return Encoded hop bytes to be concatenated for multi-hop or passed directly for single-hop. + /// @dev Format depends on command: + /// - DistributeNative: [cmd(1)][numPools(1)=1][share(2)=FULL][len(2)][data] + /// - DispatchSinglePoolSwap: [cmd(1)][tokenIn(20)][len(2)][data] + /// - Others (User/MyERC20): [cmd(1)][tokenIn(20)][numPools(1)=1][share(2)=FULL][len(2)][data] + /// @custom:example Single-hop user ERC20 + /// bytes memory data = abi.encodePacked(facet.swapUniV2.selector, pool, uint8(1), destinationAddress); + /// bytes memory route = _buildBaseRoute(params, data); + function _buildBaseRoute( + SwapTestParams memory params, + bytes memory swapData + ) internal pure returns (bytes memory) { + if (params.commandType == CommandType.DistributeNative) { + return + abi.encodePacked( + uint8(params.commandType), + uint8(1), + FULL_SHARE, + uint16(swapData.length), + swapData + ); + } else if (params.commandType == CommandType.DispatchSinglePoolSwap) { + return + abi.encodePacked( + uint8(params.commandType), + params.tokenIn, + uint16(swapData.length), + swapData + ); + } else { + return + abi.encodePacked( + uint8(params.commandType), + params.tokenIn, + uint8(1), + FULL_SHARE, + uint16(swapData.length), + swapData + ); + } + } + + /// @notice Executes a built route and verifies balances and events with full verification options + /// @param params Swap parameters including token addresses, amounts, and command type + /// @param route Pre-built route bytes (single or multi-hop) + /// @param additionalEvents Additional external events to expect during execution + /// @param isFeeOnTransferToken Whether tokenIn is fee-on-transfer token (tolerates off-by-1 spent) + /// @param routeEventVerification Route event check configuration for exact output and data validation + /// @dev Handles the following: + /// - Approves tokenIn if not aggregator-funded + /// - Tracks balances before/after for both input and output tokens + /// - Emits and verifies Route event with specified verification options + /// - Supports native token transfers via msg.value + /// - Validates token spent matches expected amount (with fee-on-transfer tolerance) + /// - Ensures positive output amount received + function _executeAndVerifySwap( + SwapTestParams memory params, + bytes memory route, + ExpectedEvent[] memory additionalEvents, + bool isFeeOnTransferToken, + RouteEventVerification memory routeEventVerification + ) internal { + if ( + params.commandType != CommandType.DistributeSelfERC20 && + !LibAsset.isNativeAsset(params.tokenIn) + ) { + IERC20(params.tokenIn).approve( + address(ldaDiamond), + params.amountIn + ); + } + + uint256 inBefore; + uint256 outBefore = LibAsset.isNativeAsset(params.tokenOut) + ? params.destinationAddress.balance + : IERC20(params.tokenOut).balanceOf(params.destinationAddress); + + // For aggregator funds, check the diamond's balance + if (params.commandType == CommandType.DistributeSelfERC20) { + inBefore = LibAsset.isNativeAsset(params.tokenIn) + ? address(ldaDiamond).balance + : IERC20(params.tokenIn).balanceOf(address(ldaDiamond)); + } else { + inBefore = LibAsset.isNativeAsset(params.tokenIn) + ? params.sender.balance + : IERC20(params.tokenIn).balanceOf(params.sender); + } + + address fromAddress = params.sender == address(ldaDiamond) + ? USER_SENDER + : params.sender; + + _expectEvents(additionalEvents); + + vm.expectEmit( + true, + true, + true, + routeEventVerification.checkData, + address(ldaDiamond) + ); + emit Route( + fromAddress, + params.destinationAddress, + params.tokenIn, + params.tokenOut, + params.amountIn, + params.minOut, + routeEventVerification.expectedExactOut + ); + + // For native token, send value with the call + if (LibAsset.isNativeAsset(params.tokenIn)) { + coreRouteFacet.processRoute{ value: params.amountIn }( + params.tokenIn, + params.amountIn, + params.tokenOut, + params.minOut, + params.destinationAddress, + route + ); + } else { + coreRouteFacet.processRoute( + params.tokenIn, + params.amountIn, + params.tokenOut, + params.minOut, + params.destinationAddress, + route + ); + } + + uint256 inAfter; + uint256 outAfter = LibAsset.isNativeAsset(params.tokenOut) + ? params.destinationAddress.balance + : IERC20(params.tokenOut).balanceOf(params.destinationAddress); + + // Check balance change on the correct address + if (params.commandType == CommandType.DistributeSelfERC20) { + inAfter = LibAsset.isNativeAsset(params.tokenIn) + ? address(ldaDiamond).balance + : IERC20(params.tokenIn).balanceOf(address(ldaDiamond)); + } else { + inAfter = LibAsset.isNativeAsset(params.tokenIn) + ? params.sender.balance + : IERC20(params.tokenIn).balanceOf(params.sender); + } + + // Use assertEq or assertApproxEqAbs based on isFeeOnTransferToken + if (isFeeOnTransferToken) { + assertApproxEqAbs( + inBefore - inAfter, + params.amountIn, + 1, // Allow 1 wei difference for fee-on-transfer tokens + "Token spent mismatch" + ); + } else { + assertEq( + inBefore - inAfter, + params.amountIn, + "Token spent mismatch" + ); + } + + assertGt(outAfter - outBefore, 0, "Should receive tokens"); + } + + /// @notice Executes a built route with basic verification and no exact output check + /// @param params Swap parameters including token addresses, amounts, and command type + /// @param route Pre-built route bytes (single or multi-hop) + /// @param additionalEvents Additional external events to expect during execution + /// @param isFeeOnTransferToken Whether tokenIn is fee-on-transfer token (tolerates off-by-1 spent) + /// @dev Convenience overload that: + /// - Sets expectedExactOut to 0 (no exact output verification) + /// - Disables Route event data validation + /// - Maintains all other verification steps from the full version + function _executeAndVerifySwap( + SwapTestParams memory params, + bytes memory route, + ExpectedEvent[] memory additionalEvents, + bool isFeeOnTransferToken + ) internal { + _executeAndVerifySwap( + params, + route, + additionalEvents, + isFeeOnTransferToken, + RouteEventVerification({ expectedExactOut: 0, checkData: false }) + ); + } + + /// @notice Executes a built route with minimal verification + /// @param params Swap parameters including token addresses, amounts, and command type + /// @param route Pre-built route bytes (single or multi-hop) + /// @dev Simplest overload that: + /// - Assumes non-fee-on-transfer token + /// - Expects no additional events + /// - Disables exact output verification + /// - Disables Route event data validation + /// - Useful for basic swap tests without complex verification needs + function _executeAndVerifySwap( + SwapTestParams memory params, + bytes memory route + ) internal { + _executeAndVerifySwap( + params, + route, + new ExpectedEvent[](0), + false, + RouteEventVerification({ expectedExactOut: 0, checkData: false }) + ); + } + + /// @notice Executes a built route with fee-on-transfer support + /// @param params Swap parameters including token addresses, amounts, and command type + /// @param route Pre-built route bytes (single or multi-hop) + /// @param isFeeOnTransferToken Whether tokenIn is fee-on-transfer token (tolerates off-by-1 spent) + /// @dev Convenience overload that: + /// - Supports fee-on-transfer tokens via tolerance parameter + /// - Expects no additional events + /// - Disables exact output verification + /// - Disables Route event data validation + /// - Useful for testing fee-on-transfer tokens without complex event verification + function _executeAndVerifySwap( + SwapTestParams memory params, + bytes memory route, + bool isFeeOnTransferToken + ) internal { + _executeAndVerifySwap( + params, + route, + new ExpectedEvent[](0), + isFeeOnTransferToken, + RouteEventVerification({ expectedExactOut: 0, checkData: false }) + ); + } + + /// @notice Executes a route expecting a specific revert error + /// @param params Swap parameters including token addresses, amounts, and command type + /// @param route Pre-built route bytes (single or multi-hop) + /// @param expectedRevert Error selector that should be thrown by processRoute + /// @dev Special overload for testing failure cases: + /// - For aggregator funds (DistributeSelfERC20), sends amountIn-1 to trigger errors + /// - For user funds, approves full amountIn but sends amountIn-1 + /// - Sets minOut to 0 to focus on specific error cases + /// - Verifies exact error selector match + function _executeAndVerifySwap( + SwapTestParams memory params, + bytes memory route, + bytes4 expectedRevert + ) internal { + if ( + params.commandType != CommandType.DistributeSelfERC20 && + !LibAsset.isNativeAsset(params.tokenIn) + ) { + IERC20(params.tokenIn).approve( + address(ldaDiamond), + params.amountIn + ); + } + + vm.expectRevert(expectedRevert); + { + if (LibAsset.isNativeAsset(params.tokenIn)) { + coreRouteFacet.processRoute{ value: params.amountIn }( + params.tokenIn, + params.amountIn, + params.tokenOut, + 0, // minOut = 0 for tests + params.destinationAddress, + route + ); + } else { + coreRouteFacet.processRoute( + params.tokenIn, + params.amountIn, + params.tokenOut, + 0, // minOut = 0 for tests + params.destinationAddress, + route + ); + } + } + } + + /// @notice Builds route and executes swap with full verification options in a single call + /// @param params SwapTestParams for building and executing the swap + /// @param swapData DEX-specific swap data to pack into the route + /// @param expectedEvents Additional events to expect during execution + /// @param isFeeOnTransferToken Whether to allow 1 wei difference in spent amount for fee-on-transfer tokens + /// @param verification Route event verification configuration + /// @dev Comprehensive helper that: + /// - Builds route using _buildBaseRoute + /// - Executes swap with full verification options + /// - Supports all verification features: events, fee-on-transfer tokens, exact output + /// - Primarily used by complex test scenarios to keep code concise + function _buildRouteAndExecuteAndVerifySwap( + SwapTestParams memory params, + bytes memory swapData, + ExpectedEvent[] memory expectedEvents, + bool isFeeOnTransferToken, + RouteEventVerification memory verification + ) internal { + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap( + params, + route, + expectedEvents, + isFeeOnTransferToken, + verification + ); + } + + /// @notice Builds route and executes swap with default verification settings + /// @param params SwapTestParams for building and executing the swap + /// @param swapData DEX-specific swap data to pack into the route + /// @dev Simple helper that: + /// - Builds route using _buildBaseRoute + /// - Executes with default settings: + /// - No additional events + /// - No fee-on-transfer handling + /// - No exact output verification + /// - No Route event data validation + /// - Useful for basic swap test scenarios + function _buildRouteAndExecuteAndVerifySwap( + SwapTestParams memory params, + bytes memory swapData + ) internal { + _buildRouteAndExecuteAndVerifySwap( + params, + swapData, + new ExpectedEvent[](0), + false, + RouteEventVerification({ expectedExactOut: 0, checkData: false }) + ); + } + + /// @notice Builds route and executes swap expecting a specific revert error + /// @param params SwapTestParams for building and executing the swap + /// @param swapData DEX-specific swap data to pack into the route + /// @param expectedRevert Error selector that should be thrown by processRoute + /// @dev Revert testing helper that: + /// - Builds route using _buildBaseRoute + /// - Delegates to _executeAndVerifySwap's revert testing logic + /// - For aggregator funds, uses amountIn-1 to trigger errors + /// - Sets minOut to 0 to focus on specific error cases + function _buildRouteAndExecuteAndVerifySwap( + SwapTestParams memory params, + bytes memory swapData, + bytes4 expectedRevert + ) internal { + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route, expectedRevert); + } + + /// @notice Builds route and executes swap with fee-on-transfer support and event verification + /// @param params SwapTestParams for building and executing the swap + /// @param swapData DEX-specific swap data to pack into the route + /// @param additionalEvents Additional events to expect during execution + /// @param isFeeOnTransferToken Whether tokenIn is fee-on-transfer token (tolerates off-by-1 spent) + /// @dev Extended helper that: + /// - Builds route using _buildBaseRoute + /// - Supports fee-on-transfer tokens via tolerance parameter + /// - Allows verification of additional protocol events + /// - Disables exact output verification + /// - Disables Route event data validation + /// - Useful for testing complex scenarios with fee-on-transfer tokens + function _buildRouteAndExecuteAndVerifySwap( + SwapTestParams memory params, + bytes memory swapData, + ExpectedEvent[] memory additionalEvents, + bool isFeeOnTransferToken + ) internal { + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap( + params, + route, + additionalEvents, + isFeeOnTransferToken + ); + } + + /// @notice Helper to load a topic value from a 32-byte abi.encode(param). + /// @param enc A 32-byte abi-encoded static param. + /// @return topic The bytes32 topic. + function _asTopic(bytes memory enc) internal pure returns (bytes32 topic) { + if (enc.length != 32) revert InvalidTopicLength(); + assembly { + topic := mload(add(enc, 32)) + } + } + + /// @notice Sets up event expectations for a list of events + /// @param events Array of events to expect + /// @dev Each `ExpectedEvent` can independently toggle checking indexed topics and data. + function _expectEvents(ExpectedEvent[] memory events) internal { + for (uint256 i = 0; i < events.length; i++) { + _expectEvent(events[i]); + } + } + + /// @notice Sets up expectation for a single event + /// @param evt The event to expect with its check parameters and data + /// @dev Builds the right number of topics based on `indexedParamIndices`, and an ABI-packed data + /// payload of non-indexed params (static only). + function _expectEvent(ExpectedEvent memory evt) internal { + vm.expectEmit( + evt.checkTopic1, + evt.checkTopic2, + evt.checkTopic3, + evt.checkData + ); + + // Build topics (topic0 = selector; topics1..3 from indexedParamIndices) + bytes32 topic0 = evt.eventSelector; + uint8[] memory idx = evt.indexedParamIndices; + + bytes32 t1; + bytes32 t2; + bytes32 t3; + + uint256 topicsCount = idx.length; + if (topicsCount > 3) { + revert TooManyIndexedParams(); + } + if (topicsCount >= 1) { + t1 = _asTopic(evt.eventParams[idx[0]]); + } + if (topicsCount >= 2) { + t2 = _asTopic(evt.eventParams[idx[1]]); + } + if (topicsCount == 3) { + t3 = _asTopic(evt.eventParams[idx[2]]); + } + + // Build data (non-indexed params in event order) + bytes memory data; + if (evt.checkData) { + // Only support static params for now (each abi.encode(param) must be 32 bytes) + uint256 total = evt.eventParams.length; + bool[] memory isIndexed = new bool[](total); + for (uint256 k = 0; k < topicsCount; k++) { + uint8 pos = idx[k]; + if (pos >= evt.eventParams.length) { + revert InvalidIndexedParamPosition( + pos, + evt.eventParams.length + ); + } + isIndexed[pos] = true; + } + + for (uint256 p = 0; p < total; p++) { + if (!isIndexed[p]) { + bytes memory enc = evt.eventParams[p]; + if (enc.length != 32) { + revert DynamicParamsNotSupported(); + } + data = bytes.concat(data, enc); + } + } + } else { + data = ""; + } + + // Emit raw log with correct number of topics + assembly { + let ptr := add(data, 0x20) + let len := mload(data) + switch topicsCount + case 0 { + log1(ptr, len, topic0) + } + case 1 { + log2(ptr, len, topic0, t1) + } + case 2 { + log3(ptr, len, topic0, t1, t2) + } + case 3 { + log4(ptr, len, topic0, t1, t2, t3) + } + } + } +} diff --git a/test/solidity/Periphery/LDA/BaseDEXFacet.t.sol b/test/solidity/Periphery/LDA/BaseDEXFacet.t.sol new file mode 100644 index 000000000..afc5cf185 --- /dev/null +++ b/test/solidity/Periphery/LDA/BaseDEXFacet.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { BaseCoreRouteTest } from "./BaseCoreRoute.t.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +/// @title BaseDEXFacetTest +/// @notice Base test contract with common functionality and abstractions for DEX-specific tests. +/// @dev Child tests implement the virtual hooks to: +/// - choose fork/network +/// - set pool/token addresses +/// - deploy and register their DEX facet + callback (if applicable)/// +/// Usage: +/// - Inherit and implement `_setupForkConfig`, `_setupDexEnv`, +/// and facet creation hooks. +/// - Call core helpers like `_buildMultiHopRoute` and `_addDexFacet`. +abstract contract BaseDEXFacetTest is BaseCoreRouteTest { + using SafeERC20 for IERC20; + + // ==== Types ==== + + /// @notice Encapsulates a fork target used by setUp(). + /// @param networkName A key into config/networks.json and an ENV suffix for ETH_NODE_URI_. + /// @param blockNumber Block height to fork at. + struct ForkConfig { + string networkName; + uint256 blockNumber; + } + + /// @notice Describes swap direction for two-token pools. + /// @dev TokenXToTokenY values map to pool-specific zeroForOne flags. + enum SwapDirection { + Token1ToToken0, // 0 + Token0ToToken1 // 1 + } + + // ==== Events ==== + + // ==== Errors ==== + + /// @notice Thrown when a required on-chain pool does not exist. + error PoolDoesNotExist(); + /// @notice Thrown when hopParams and hopData arrays differ in length. + error ParamsDataLengthMismatch(); + /// @notice Thrown when no hops were provided to `_buildMultiHopRoute`. + error NoHopsProvided(); + /// @notice Thrown when setUp fork config is invalid. + /// @param reason A human-readable reason. + error InvalidForkConfig(string reason); + /// @notice Thrown when networkName is not found in config/networks.json. + /// @param name The missing network name. + error UnknownNetwork(string name); + + // Custom errors for abstract test functions + /// @notice Thrown when test_CanSwap is not implemented by child contract. + error TestCanSwapNotImplemented(); + /// @notice Thrown when test_CanSwap_FromDexAggregator is not implemented by child contract. + error TestCanSwapFromDexAggregatorNotImplemented(); + /// @notice Thrown when test_CanSwap_MultiHop is not implemented by child contract. + error TestCanSwapMultiHopNotImplemented(); + /// @notice Thrown when testRevert_CallbackFromUnexpectedSender is not implemented by child contract. + error TestRevertCallbackFromUnexpectedSenderNotImplemented(); + /// @notice Thrown when testRevert_SwapWithoutCallback is not implemented by child contract. + error TestRevertSwapWithoutCallbackNotImplemented(); + + // ==== Storage Variables ==== + + /// @notice Active fork settings for this test. + ForkConfig internal forkConfig; + + /// @notice Primary input token for single-hop tests. + IERC20 internal tokenIn; + /// @notice Optional middle token for multi-hop tests. + IERC20 internal tokenMid; // optional for multi-hop + /// @notice Primary output token for single-hop tests. + IERC20 internal tokenOut; + + /// @notice Pool for tokenIn->tokenOut single-hop routes or UniV2-style. + address internal poolInOut; // for single hop or UniV2-style + /// @notice Pool for hop 1 in multi-hop tests. + address internal poolInMid; // for hop 1 + /// @notice Pool for hop 2 in multi-hop tests. + address internal poolMidOut; // for hop 2 + + // ==== Virtual Functions ==== + + /// @notice Child must deploy its facet and return its function selectors. + /// @return facetAddress Address of the facet implementation. + /// @return functionSelectors Selectors to add to the diamond. + function _createFacetAndSelectors() + internal + virtual + returns (address, bytes4[] memory); + + /// @notice Child must set its facet instance to the diamond proxy. + /// @param ldaDiamond Address of the diamond proxy that now exposes the facet. + function _setFacetInstance(address payable ldaDiamond) internal virtual; + + /// @notice Child must configure tokens/pools for the selected fork. + function _setupDexEnv() internal virtual; + + /// @notice Child must set `forkConfig` with target network + block. + function _setupForkConfig() internal virtual; + + // ==== Setup Functions ==== + + /// @notice Forks the configured network, validates ENV, and attaches the DEX facet to LDA. + /// @dev Validates: + /// - ENV var ETH_NODE_URI_ + /// - key existence in config/networks.json + /// - non-zero block number + /// Then forks and sets up the DEX env and adds the DEX facet to the LDA diamond. + function setUp() public virtual override { + _setupForkConfig(); + + // Validate network name + if (bytes(forkConfig.networkName).length == 0) { + revert InvalidForkConfig("networkName not set"); + } + + // Validate block number + if (forkConfig.blockNumber == 0) { + revert InvalidForkConfig("blockNumber not set"); + } + + // Compute RPC URL and validate it exists + string memory rpc = string.concat( + "ETH_NODE_URI_", + _convertToUpperCase(forkConfig.networkName) + ); + + try vm.envString(rpc) returns (string memory rpcUrl) { + if (bytes(rpcUrl).length == 0) { + revert InvalidForkConfig("RPC URL is empty"); + } + customRpcUrlForForking = rpc; + } catch { + revert InvalidForkConfig("RPC URL not found"); + } + + // optional validation against networks.json + _ensureNetworkExists(forkConfig.networkName); + + customBlockNumberForForking = forkConfig.blockNumber; + + fork(); + super.setUp(); + _setupDexEnv(); // populate tokens/pools + _addDexFacet(); + } + + // ==== Internal Functions ==== + + /// @notice Deploys and adds the child DEX facet to the LDA diamond. + /// @dev Uses `_createFacetAndSelectors` hook to deploy and collect selectors, + /// performs diamondCut, and then updates the facet instance to the diamond proxy via `_setFacetInstance`. + function _addDexFacet() internal virtual { + ( + address facetAddress, + bytes4[] memory functionSelectors + ) = _createFacetAndSelectors(); + + addFacet(address(ldaDiamond), facetAddress, functionSelectors); + + _setFacetInstance(payable(address(ldaDiamond))); + } + + // ==== Helper Functions ==== + + /// @notice Converts lowercase ASCII to uppercase; leaves non-letters unmodified. + /// @param s Input string. + /// @return Uppercased string. + function _convertToUpperCase( + string memory s + ) internal pure returns (string memory) { + bytes memory b = bytes(s); + for (uint256 i; i < b.length; i++) { + uint8 c = uint8(b[i]); + if (c >= 97 && c <= 122) { + b[i] = bytes1(c - 32); + } + } + return string(b); + } + + /// @notice Ensures a network key exists in `config/networks.json`. + /// @param name Network name to validate. + /// @dev Reads `..name` and reverts if missing. + function _ensureNetworkExists(string memory name) internal { + // will revert if the key path is missing + string memory json = vm.readFile("config/networks.json"); + // read the "..name" path to confirm key presence + string memory path = string.concat(".", name, ".name"); + string memory value = stdJson.readString(json, path); + if (bytes(value).length == 0) { + revert UnknownNetwork(name); + } + } + + /// @notice Concatenates multiple base routes into a single multi-hop route for `processRoute`. + /// @param hopParams Array of hop parameters (tokenIn/out, amountIn, sender/destinationAddress, command type). + /// @param hopData Array of corresponding DEX-specific swap data for each hop. + /// @return Concatenated route bytes suitable for `CoreRouteFacet.processRoute`. + /// @dev Reverts if arrays mismatch or empty. Example: + /// - Hop0: user ERC20 A -> aggregator, UniV2.pool0 + /// - Hop1: aggregator ERC20 B -> user, Algebra.pool1 (supports fee-on-transfer) + function _buildMultiHopRoute( + SwapTestParams[] memory hopParams, + bytes[] memory hopData + ) internal pure returns (bytes memory) { + if (hopParams.length != hopData.length) + revert ParamsDataLengthMismatch(); + if (hopParams.length == 0) revert NoHopsProvided(); + + bytes memory route; + for (uint256 i = 0; i < hopParams.length; i++) { + route = bytes.concat( + route, + _buildBaseRoute(hopParams[i], hopData[i]) + ); + } + return route; + } + + /// @notice Default amount for `tokenIn` used by derived tests. + /// @return Default amount, override to adapt per pool/tokenIn/decimals. + function _getDefaultAmountForTokenIn() + internal + pure + virtual + returns (uint256) + { + return 1_000 * 1e18; + } + + // ==== Abstract Test Cases ==== + + /// @notice Abstract test stub: must be implemented by concrete DEX tests to validate basic swapping. + function test_CanSwap() public virtual { + // Each DEX implementation must override this + revert TestCanSwapNotImplemented(); + } + + /// @notice Abstract test stub: must be implemented by concrete DEX tests to validate aggregator-funded swap. + function test_CanSwap_FromDexAggregator() public virtual { + // Each DEX implementation must override this + revert TestCanSwapFromDexAggregatorNotImplemented(); + } + + /// @notice Abstract test stub: must be implemented by concrete DEX tests to validate multi-hop routing. + function test_CanSwap_MultiHop() public virtual { + // Each DEX implementation must override this + revert TestCanSwapMultiHopNotImplemented(); + } + + /// @notice Abstract test stub: for DEXes with callbacks, ensure callback cannot be called by unexpected senders. + function testRevert_CallbackFromUnexpectedSender() public virtual { + // Each DEX implementation with callbacks must override this + // DEXs without callbacks should leave this empty + revert TestRevertCallbackFromUnexpectedSenderNotImplemented(); + } + + /// @notice Abstract test stub: for DEXes with callbacks, ensure swap reverts if callback is not executed. + function testRevert_SwapWithoutCallback() public virtual { + // Each DEX implementation with callbacks must override this + // DEXs without callbacks should leave this empty + revert TestRevertSwapWithoutCallbackNotImplemented(); + } +} diff --git a/test/solidity/Periphery/LDA/BaseDEXFacetWithCallback.t.sol b/test/solidity/Periphery/LDA/BaseDEXFacetWithCallback.t.sol new file mode 100644 index 000000000..1097e149b --- /dev/null +++ b/test/solidity/Periphery/LDA/BaseDEXFacetWithCallback.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LibCallbackAuthenticator } from "lifi/Libraries/LibCallbackAuthenticator.sol"; +import { SwapCallbackNotExecuted } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; +import { BaseDEXFacetTest } from "./BaseDEXFacet.t.sol"; +import { MockNoCallbackPool } from "../../utils/MockNoCallbackPool.sol"; + +/// @title BaseDEXFacetWithCallbackTest +/// @notice Base harness for testing DEX facets that rely on swap callbacks. +/// @dev Provides callback selector/data hooks and two negative tests: +/// - unexpected callback sender +/// - swap path where pool never calls back (should revert) +abstract contract BaseDEXFacetWithCallbackTest is BaseDEXFacetTest { + /// @notice Returns the callback selector used by the DEX under test. + /// @return selector Function selector for the DEX's swap callback. + function _getCallbackSelector() internal virtual returns (bytes4); + + /// @notice Builds swap data that arms callback verification for the DEX under test. + /// @param pool Pool expected to invoke the callback. + /// @param destinationAddress Destination address of swap proceeds. + /// @return swapData Encoded payload that triggers the DEX callback path. + function _buildCallbackSwapData( + address pool, + address destinationAddress + ) internal virtual returns (bytes memory); + + /// @notice Provides a mock pool that never performs the callback (negative path). + /// @return pool Address of a pool that will not call the callback. + function _deployNoCallbackPool() internal virtual returns (address) { + return address(new MockNoCallbackPool()); + } + + /// @notice Reverts when the callback is invoked by an unexpected sender. + /// @dev No swap is performed beforehand, so the authenticator should hold address(0) and reject. + function testRevert_CallbackFromUnexpectedSender() + public + virtual + override + { + // No swap has armed the guard; expected == address(0) + vm.startPrank(USER_SENDER); + vm.expectRevert( + LibCallbackAuthenticator.UnexpectedCallbackSender.selector + ); + (bool ok, ) = address(ldaDiamond).call( + abi.encodeWithSelector( + _getCallbackSelector(), + int256(1), + int256(1), + bytes("") + ) + ); + ok; + vm.stopPrank(); + } + + /// @notice Reverts when the swap path never executes the callback. + /// @dev Uses a mock pool that does not call back; the aggregator remains armed and must revert. + function testRevert_SwapWithoutCallback() public virtual override { + // Pool that does not call back (facet-specific implementation) + address mockPool = _deployNoCallbackPool(); + + // Setup test params + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + // Build facet-specific swap data + bytes memory swapData = _buildCallbackSwapData(mockPool, USER_SENDER); + + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + // Should revert because pool doesn't call back, leaving armed state + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route, + SwapCallbackNotExecuted.selector + ); + + vm.stopPrank(); + } +} diff --git a/test/solidity/Periphery/LDA/BaseUniV2StyleDEXFacet.t.sol b/test/solidity/Periphery/LDA/BaseUniV2StyleDEXFacet.t.sol new file mode 100644 index 000000000..c56075d10 --- /dev/null +++ b/test/solidity/Periphery/LDA/BaseUniV2StyleDEXFacet.t.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UniV2StyleFacet } from "lifi/Periphery/LDA/Facets/UniV2StyleFacet.sol"; +import { IUniV3StylePool } from "lifi/Interfaces/IUniV3StylePool.sol"; +import { BaseDEXFacetTest } from "./BaseDEXFacet.t.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; + +/// @title BaseUniV2StyleDEXFacetTest +/// @notice Shared UniV2-style testing helpers built atop BaseDEXFacetTest. +/// @dev Handles selector wiring, pool direction inference (token0/token1), and auto-execution flows. +abstract contract BaseUniV2StyleDEXFacetTest is BaseDEXFacetTest { + /// @notice UniV2-style facet proxy handle (points to diamond after setup). + UniV2StyleFacet internal uniV2Facet; + + // ==== Types ==== + + /// @notice Parameters for a single UniV2-style swap step. + /// @param pool Target pool address. + /// @param direction Direction of the swap (token0->token1 or vice versa). + /// @param destinationAddress Destination address of the proceeds. + struct UniV2SwapParams { + address pool; + SwapDirection direction; + address destinationAddress; + uint24 fee; + } + + /// @notice Parameters for convenience auto-execution. + /// @param commandType Whether to use aggregator funds or user funds. + /// @param amountIn Input amount to test with. + struct UniV2AutoSwapParams { + CommandType commandType; + uint256 amountIn; + } + + // ==== Errors ==== + + /// @notice Thrown when a pool does not include the provided token. + /// @param token Token address not found in pool. + /// @param pool Pool address. + error TokenNotInPool(address token, address pool); + + // ==== Setup Functions ==== + + /// @notice Deploys UniV2Style facet and registers `swapUniV2` + DEX-specific callback selector. + /// @return facetAddress Address of the deployed facet implementation. + /// @return functionSelectors Selectors for swap and callback. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + uniV2Facet = new UniV2StyleFacet(); + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = uniV2Facet.swapUniV2.selector; + return (address(uniV2Facet), functionSelectors); + } + + /// @notice Sets `uniV2Facet` to the diamond proxy (post-cut). + /// @param ldaDiamond Diamond proxy address. + function _setFacetInstance(address payable ldaDiamond) internal override { + uniV2Facet = UniV2StyleFacet(ldaDiamond); + } + + // ==== Helper Functions ==== + + /// @notice Virtual function to get the pool fee. Child contracts should override this. + /// @return fee The pool fee in basis points (e.g., 3000 for 0.3%) + function _getPoolFee() internal virtual returns (uint24); + + /// @notice Builds packed swap data for UniV2-style swap dispatch. + /// @param params Struct including pool, direction and destinationAddress. + /// @return Packed payload starting with `swapUniV2` selector. + function _buildUniV2SwapData( + UniV2SwapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + uniV2Facet.swapUniV2.selector, + params.pool, + uint8(params.direction), + params.destinationAddress, + params.fee + ); + } + + /// @notice Executes a UniV2-style swap for arbitrary pool and direction. + /// @param params Swap test params (sender/destinationAddress/funding mode). + /// @param pool Pool to swap on. + /// @param direction Swap direction to use. + /// @dev Funds sender or diamond accordingly, builds route and executes with default assertions. + function _executeUniV2StyleSwap( + SwapTestParams memory params, + address pool, + SwapDirection direction + ) internal { + // Fund the appropriate account + if (params.commandType == CommandType.DistributeSelfERC20) { + // if tokens come from the aggregator (address(ldaDiamond)), use command code 1; otherwise, use 2. + deal(params.tokenIn, address(ldaDiamond), params.amountIn + 1); + } else { + deal(params.tokenIn, params.sender, params.amountIn); + } + + vm.startPrank(params.sender); + + bytes memory swapData = _buildUniV2SwapData( + UniV2SwapParams({ + pool: pool, + direction: direction, + destinationAddress: params.destinationAddress, + fee: _getPoolFee() + }) + ); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route); + + vm.stopPrank(); + } + + /// @notice Infers direction (token0->token1 or token1->token0) given a pool and `tokenIn`. + /// @param pool The target UniV2-style pool. + /// @param tokenIn The input token address. + /// @return Inferred direction. + function _getDirection( + address pool, + address tokenIn + ) internal view returns (SwapDirection) { + address t0 = IUniV3StylePool(pool).token0(); + address t1 = IUniV3StylePool(pool).token1(); + if (tokenIn == t0) return SwapDirection.Token0ToToken1; + if (tokenIn == t1) return SwapDirection.Token1ToToken0; + revert TokenNotInPool(tokenIn, pool); + } + + /// @notice Convenience flow: infers direction from `poolInOut` and executes with given funding mode. + /// @param params commandType + amountIn + /// @dev Funds sender or diamond, builds route for `poolInOut`, and executes. + function _executeUniV2StyleSwapAuto( + UniV2AutoSwapParams memory params + ) internal { + uint256 amountIn = params.amountIn; + + // Fund the appropriate account + if (params.commandType == CommandType.DistributeSelfERC20) { + deal(address(tokenIn), address(ldaDiamond), amountIn + 1); + } else { + deal(address(tokenIn), USER_SENDER, amountIn); + } + + vm.startPrank(USER_SENDER); + + SwapDirection direction = _getDirection(poolInOut, address(tokenIn)); + bytes memory swapData = _buildUniV2SwapData( + UniV2SwapParams({ + pool: poolInOut, + direction: direction, + destinationAddress: USER_SENDER, + fee: _getPoolFee() // Replace params.fee with dynamic fee + }) + ); + + // Build route and execute + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: params.commandType == CommandType.DistributeSelfERC20 + ? address(ldaDiamond) + : USER_SENDER, + destinationAddress: USER_SENDER, + commandType: params.commandType + }), + swapData + ); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: params.commandType == CommandType.DistributeSelfERC20 + ? address(ldaDiamond) + : USER_SENDER, + destinationAddress: USER_SENDER, + commandType: params.commandType + }), + route + ); + + vm.stopPrank(); + } + + // ==== Test Cases ==== + + /// @notice Tests that the facet reverts when pool address is zero + function testRevert_InvalidPool() public { + vm.startPrank(USER_SENDER); + + bytes memory swapDataZeroPool = _buildUniV2SwapData( + UniV2SwapParams({ + pool: address(0), + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_SENDER, + fee: 3000 // 0.3% standard fee + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataZeroPool, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Tests that the facet reverts when destination address is zero + function testRevert_InvalidDestinationAddress() public { + vm.startPrank(USER_SENDER); + + bytes memory swapDataZeroDestination = _buildUniV2SwapData( + UniV2SwapParams({ + pool: poolInOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: address(0), + fee: 3000 + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataZeroDestination, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Tests that the facet reverts when fee is invalid (>= FEE_DENOMINATOR) + function testRevert_InvalidFee() public { + vm.startPrank(USER_SENDER); + + bytes memory swapDataInvalidFee = _buildUniV2SwapData( + UniV2SwapParams({ + pool: poolInOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_SENDER, + fee: 1_000_000 // Equal to FEE_DENOMINATOR + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataInvalidFee, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Intentionally skipped: UniV2 multi-hop unsupported due to amountSpecified=0 limitation on second hop. + function test_CanSwap_MultiHop() public virtual override { + // SKIPPED: UniV2 forke dex multi-hop unsupported due to AS (amount specified) requirement. + // UniV2 forke dex does not support a "one-pool" second hop today, + // because the aggregator (ProcessOnePool) always passes amountSpecified = 0 into + // the pool.swap call. UniV2-style pools immediately revert on + // require(amountSpecified != 0, 'AS'), so you can't chain two uniV2 pools + // in a single processRoute invocation. + } + + /// @notice Empty test as UniV2-style dexes does not use callbacks + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + function testRevert_CallbackFromUnexpectedSender() public override { + // UniV2-style dexes does not use callbacks - test intentionally empty + } + + /// @notice Empty test as UniV2-style dexes does not use callbacks + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + function testRevert_SwapWithoutCallback() public override { + // UniV2-style dexes does not use callbacks - test intentionally empty + } + + /// @notice User-funded single-hop swap on UniV2-style pool inferred from `poolInOut`. + function test_CanSwap() public virtual override { + _executeUniV2StyleSwapAuto( + UniV2AutoSwapParams({ + commandType: CommandType.DistributeUserERC20, + amountIn: _getDefaultAmountForTokenIn() + }) + ); + } + + /// @notice Aggregator-funded single-hop swap on UniV2-style. + function test_CanSwap_FromDexAggregator() public virtual override { + _executeUniV2StyleSwapAuto( + UniV2AutoSwapParams({ + commandType: CommandType.DistributeSelfERC20, + amountIn: _getDefaultAmountForTokenIn() + }) + ); + } +} diff --git a/test/solidity/Periphery/LDA/BaseUniV3StyleDEXFacet.t.sol b/test/solidity/Periphery/LDA/BaseUniV3StyleDEXFacet.t.sol new file mode 100644 index 000000000..f19373acd --- /dev/null +++ b/test/solidity/Periphery/LDA/BaseUniV3StyleDEXFacet.t.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { IUniV3StylePool } from "lifi/Interfaces/IUniV3StylePool.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseDEXFacetWithCallbackTest } from "./BaseDEXFacetWithCallback.t.sol"; + +/// @title BaseUniV3StyleDEXFacetTest +/// @notice Shared UniV3-style testing helpers built atop BaseDEXFacetWithCallbackTest. +/// @dev Handles selector wiring, pool direction inference (token0/token1), and auto-execution flows. +abstract contract BaseUniV3StyleDEXFacetTest is BaseDEXFacetWithCallbackTest { + /// @notice UniV3-style facet proxy handle (points to diamond after setup). + UniV3StyleFacet internal uniV3Facet; + + // ==== Types ==== + + /// @notice Parameters for a single UniV3-style swap step. + /// @param pool Target pool address. + /// @param direction Direction of the swap (token0->token1 or vice versa). + /// @param destinationAddress Destination address of the proceeds. + struct UniV3SwapParams { + address pool; + SwapDirection direction; + address destinationAddress; + } + + /// @notice Parameters for convenience auto-execution. + /// @param commandType Whether to use aggregator funds or user funds. + /// @param amountIn Input amount to test with. + struct UniV3AutoSwapParams { + CommandType commandType; + uint256 amountIn; + } + + // ==== Errors ==== + + /// @notice Thrown when a pool does not include the provided token. + /// @param token Token address not found in pool. + /// @param pool Pool address. + error TokenNotInPool(address token, address pool); + + // ==== Setup Functions ==== + + /// @notice Deploys UniV3Style facet and registers `swapUniV3` + DEX-specific callback selector. + /// @return facetAddress Address of the deployed facet implementation. + /// @return functionSelectors Selectors for swap and callback. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + uniV3Facet = new UniV3StyleFacet(); + bytes4[] memory functionSelectors = new bytes4[](2); + functionSelectors[0] = uniV3Facet.swapUniV3.selector; + functionSelectors[1] = _getCallbackSelector(); // Each implementation provides its specific callback + return (address(uniV3Facet), functionSelectors); + } + + /// @notice Sets `uniV3Facet` to the diamond proxy (post-cut). + /// @param ldaDiamond Diamond proxy address. + function _setFacetInstance(address payable ldaDiamond) internal override { + uniV3Facet = UniV3StyleFacet(ldaDiamond); + } + + // ==== Helper Functions ==== + + /// @notice Builds packed swap data for UniV3-style swap dispatch. + /// @param params Struct including pool, direction and destinationAddress. + /// @return Packed payload starting with `swapUniV3` selector. + function _buildUniV3SwapData( + UniV3SwapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + uniV3Facet.swapUniV3.selector, + params.pool, + uint8(params.direction), + params.destinationAddress + ); + } + + /// @notice Executes a UniV3-style swap for arbitrary pool and direction. + /// @param params Swap test params (sender/destinationAddress/funding mode). + /// @param pool Pool to swap on. + /// @param direction Swap direction to use. + /// @dev Funds sender or diamond accordingly, builds route and executes with default assertions. + function _executeUniV3StyleSwap( + SwapTestParams memory params, + address pool, + SwapDirection direction + ) internal { + // Fund the appropriate account + if (params.commandType == CommandType.DistributeSelfERC20) { + // if tokens come from the aggregator (address(ldaDiamond)), use command code 1; otherwise, use 2. + deal(params.tokenIn, address(ldaDiamond), params.amountIn + 1); + } else { + deal(params.tokenIn, params.sender, params.amountIn); + } + + vm.startPrank(params.sender); + + bytes memory swapData = _buildUniV3SwapData( + UniV3SwapParams({ + pool: pool, + direction: direction, + destinationAddress: params.destinationAddress + }) + ); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route); + + vm.stopPrank(); + } + + /// @notice Infers direction (token0->token1 or token1->token0) given a pool and `tokenIn`. + /// @param pool The target UniV3-style pool. + /// @param tokenIn The input token address. + /// @return Inferred direction. + function _getDirection( + address pool, + address tokenIn + ) internal view returns (SwapDirection) { + address t0 = IUniV3StylePool(pool).token0(); + address t1 = IUniV3StylePool(pool).token1(); + if (tokenIn == t0) return SwapDirection.Token0ToToken1; + if (tokenIn == t1) return SwapDirection.Token1ToToken0; + revert TokenNotInPool(tokenIn, pool); + } + + /// @notice Convenience flow: infers direction from `poolInOut` and executes with given funding mode. + /// @param params commandType + amountIn + /// @dev Funds sender or diamond, builds route for `poolInOut`, and executes. + function _executeUniV3StyleSwapAuto( + UniV3AutoSwapParams memory params + ) internal { + uint256 amountIn = params.amountIn; + + // Fund the appropriate account + if (params.commandType == CommandType.DistributeSelfERC20) { + deal(address(tokenIn), address(ldaDiamond), amountIn + 1); + } else { + deal(address(tokenIn), USER_SENDER, amountIn); + } + + vm.startPrank(USER_SENDER); + + SwapDirection direction = _getDirection(poolInOut, address(tokenIn)); + bytes memory swapData = _buildUniV3SwapData( + UniV3SwapParams({ + pool: poolInOut, + direction: direction, + destinationAddress: USER_SENDER + }) + ); + + // Build route and execute + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: params.commandType == CommandType.DistributeSelfERC20 + ? address(ldaDiamond) + : USER_SENDER, + destinationAddress: USER_SENDER, + commandType: params.commandType + }), + swapData + ); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: params.commandType == CommandType.DistributeSelfERC20 + ? address(ldaDiamond) + : USER_SENDER, + destinationAddress: USER_SENDER, + commandType: params.commandType + }), + route + ); + + vm.stopPrank(); + } + + // ==== Overrides ==== + + /// @notice Builds callback-arming swap data for `BaseDEXFacetWithCallbackTest` harness. + /// @param pool Pool to invoke against in callback tests. + /// @param destinationAddress Destination address for proceeds in these tests. + /// @return Packed swap payload. + function _buildCallbackSwapData( + address pool, + address destinationAddress + ) internal view override returns (bytes memory) { + return + _buildUniV3SwapData( + UniV3SwapParams({ + pool: pool, + direction: SwapDirection.Token0ToToken1, + destinationAddress: destinationAddress + }) + ); + } + + // ==== Test Cases ==== + + /// @notice Intentionally skipped: UniV3 multi-hop unsupported due to amountSpecified=0 limitation on second hop. + function test_CanSwap_MultiHop() public virtual override { + // SKIPPED: UniV3 forke dex multi-hop unsupported due to AS (amount specified) requirement. + // UniV3 forke dex does not support a "one-pool" second hop today, + // because the aggregator (ProcessOnePool) always passes amountSpecified = 0 into + // the pool.swap call. UniV3-style pools immediately revert on + // require(amountSpecified != 0, 'AS'), so you can't chain two uniV3 pools + // in a single processRoute invocation. + } + + /// @notice User-funded single-hop swap on UniV3-style pool inferred from `poolInOut`. + function test_CanSwap() public virtual override { + _executeUniV3StyleSwapAuto( + UniV3AutoSwapParams({ + commandType: CommandType.DistributeUserERC20, + amountIn: _getDefaultAmountForTokenIn() + }) + ); + } + + /// @notice Aggregator-funded single-hop swap on UniV3-style. + function test_CanSwap_FromDexAggregator() public virtual override { + _executeUniV3StyleSwapAuto( + UniV3AutoSwapParams({ + commandType: CommandType.DistributeSelfERC20, + amountIn: _getDefaultAmountForTokenIn() + }) + ); + } + + /// @notice Tests that swaps with amountIn > type(int256).max revert + function testBase_Revert_SwapUniV3WithAmountOverInt256Max() public { + uint256 amountIn = uint256(type(int256).max) + 10; + + // Fund the sender + deal(address(tokenIn), USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + SwapDirection direction = _getDirection(poolInOut, address(tokenIn)); + bytes memory swapData = _buildUniV3SwapData( + UniV3SwapParams({ + pool: poolInOut, + direction: direction, + destinationAddress: USER_SENDER + }) + ); + + // Build route and execute with expected revert + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/AlgebraFacet.t.sol b/test/solidity/Periphery/LDA/Facets/AlgebraFacet.t.sol new file mode 100644 index 000000000..83e2f41f4 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/AlgebraFacet.t.sol @@ -0,0 +1,897 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IAlgebraPool } from "lifi/Interfaces/IAlgebraPool.sol"; +import { IAlgebraRouter } from "lifi/Interfaces/IAlgebraRouter.sol"; +import { IAlgebraFactory } from "lifi/Interfaces/IAlgebraFactory.sol"; +import { IAlgebraQuoter } from "lifi/Interfaces/IAlgebraQuoter.sol"; +import { AlgebraFacet } from "lifi/Periphery/LDA/Facets/AlgebraFacet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { SwapCallbackNotExecuted } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; +import { TestToken as ERC20 } from "../../../utils/TestToken.sol"; +import { MockFeeOnTransferToken } from "../../../utils/MockTokenFeeOnTransfer.sol"; +import { BaseDEXFacetWithCallbackTest } from "../BaseDEXFacetWithCallback.t.sol"; + +/// @title AlgebraFacetTest +/// @notice Forked tests for Algebra pools integrated via LDA CoreRoute. +/// @dev Covers: +/// - user-funded and aggregator-funded swaps +/// - fee-on-transfer compatibility +/// - multi-hop routes combining user and aggregator steps +/// - callback protection and revert scenarios +contract AlgebraFacetTest is BaseDEXFacetWithCallbackTest { + /// @notice Facet proxy handle exposed on the diamond. + AlgebraFacet internal algebraFacet; + + // ==== Constants ==== + + /// @notice Algebra factory on ApeChain used during fork tests. + address private constant ALGEBRA_FACTORY_APECHAIN = + 0x10aA510d94E094Bd643677bd2964c3EE085Daffc; + /// @notice Algebra QuoterV2 on ApeChain used to get expected outputs. + address private constant ALGEBRA_QUOTER_V2_APECHAIN = + 0x60A186019F81bFD04aFc16c9C01804a04E79e68B; + /// @notice Random APE_ETH holder used to fund aggregator or users in fork tests. + address private constant RANDOM_APE_ETH_HOLDER_APECHAIN = + address(0x1EA5Df273F1b2e0b10554C8F6f7Cc7Ef34F6a51b); + + // ==== Types ==== + + /// @notice Parameters for the high-level `_testSwap` helper. + /// @param from Logical sender (user or aggregator/diamond). + /// @param destinationAddress Destination address of swap. + /// @param tokenIn Input token. + /// @param amountIn Input amount. + /// @param tokenOut Output token. + /// @param direction Direction (Token0->Token1 or reverse). + /// @param supportsFeeOnTransfer Toggle fee-on-transfer compatibility in swap data. + struct AlgebraSwapTestParams { + address from; + address destinationAddress; + address tokenIn; + uint256 amountIn; + address tokenOut; + SwapDirection direction; + bool supportsFeeOnTransfer; + } + + /// @notice Local multi-hop test state used to assemble two Algebra pools and tokens. + /// @param tokenA First token. + /// @param tokenB Middle token (can be fee-on-transfer). + /// @param tokenC Final token. + /// @param pool1 Pool for A<->B. + /// @param pool2 Pool for B<->C. + /// @param amountIn User's first-hop input amount. + /// @param amountToTransfer Funding amount for user. + /// @param isFeeOnTransfer Whether tokenB charges fees. + struct MultiHopTestState { + IERC20 tokenA; + IERC20 tokenB; // Can be either regular ERC20 or MockFeeOnTransferToken + IERC20 tokenC; + address pool1; + address pool2; + uint256 amountIn; + uint256 amountToTransfer; + bool isFeeOnTransfer; + } + + /// @notice Parameters to pack swap data for AlgebraFacet. + /// @param commandCode Command determining source of funds. + /// @param tokenIn Input token address. + /// @param destinationAddress Destination address. + /// @param pool Algebra pool. + /// @param supportsFeeOnTransfer Toggle fee-on-transfer handling. + struct AlgebraRouteParams { + CommandType commandCode; // 1 for contract funds, 2 for user funds + address tokenIn; // Input token address + address destinationAddress; // Address receiving the output tokens + address pool; // Algebra pool address + bool supportsFeeOnTransfer; // Whether to support fee-on-transfer tokens + } + + // ==== Setup Functions ==== + + /// @notice Selects `apechain` fork and block height used in tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "apechain", + blockNumber: 12912470 + }); + } + + /// @notice Deploys AlgebraFacet and registers `swapAlgebra` and `algebraSwapCallback`. + /// @return facetAddress Implementation address. + /// @return functionSelectors Selectors list (swap + callback). + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + algebraFacet = new AlgebraFacet(); + bytes4[] memory functionSelectors = new bytes4[](2); + functionSelectors[0] = algebraFacet.swapAlgebra.selector; + functionSelectors[1] = algebraFacet.algebraSwapCallback.selector; + return (address(algebraFacet), functionSelectors); + } + + /// @notice Binds `algebraFacet` to the diamond proxy after diamondCut. + /// @param ldaDiamond Diamond proxy address. + function _setFacetInstance(address payable ldaDiamond) internal override { + algebraFacet = AlgebraFacet(ldaDiamond); + } + + /// @notice Sets fork-based token/pool addresses used by tests. + /// @dev tokenIn/tokenOut chosen for ApeChain, poolInOut resolved accordingly. + function _setupDexEnv() internal override { + tokenIn = IERC20(0xcF800F4948D16F23333508191B1B1591daF70438); // APE_ETH_TOKEN + tokenOut = IERC20(0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949); // WETH_TOKEN + poolInOut = 0x217076aa74eFF7D54837D00296e9AEBc8c06d4F2; // ALGEBRA_POOL_APECHAIN + } + + /// @notice Default input amount adapted to 6 decimals for APE_ETH on ApeChain. + function _getDefaultAmountForTokenIn() + internal + pure + override + returns (uint256) + { + return 1_000 * 1e6; + } + + // ==== Test Cases ==== + + /// @notice Aggregator-funded swap on Algebra using funds transferred from a whale. + /// @dev Transfers APE_ETH to `ldaDiamond` (aggregator) and executes swap to `USER_SENDER`. + function test_CanSwap_FromDexAggregator() public override { + // Fund LDA from whale address + vm.prank(RANDOM_APE_ETH_HOLDER_APECHAIN); + IERC20(tokenIn).transfer( + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + _testSwap( + AlgebraSwapTestParams({ + from: address(ldaDiamond), + destinationAddress: address(USER_SENDER), + tokenIn: address(tokenIn), + amountIn: _getDefaultAmountForTokenIn() - 1, + tokenOut: address(tokenOut), + direction: SwapDirection.Token0ToToken1, + supportsFeeOnTransfer: true + }) + ); + + vm.stopPrank(); + } + + /// @notice User-funded swap with fee-on-transfer compatibility enabled. + /// @dev Uses `supportsFeeOnTransfer=true` to relax spending assertion in helper. + function test_CanSwap_FeeOnTransferToken() public { + vm.startPrank(RANDOM_APE_ETH_HOLDER_APECHAIN); + + // Build route for algebra swap with command code 2 (user funds) + bytes memory swapData = _buildAlgebraSwapData( + AlgebraRouteParams({ + commandCode: CommandType.DistributeUserERC20, + tokenIn: address(tokenIn), + destinationAddress: RANDOM_APE_ETH_HOLDER_APECHAIN, + pool: poolInOut, + supportsFeeOnTransfer: true + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: RANDOM_APE_ETH_HOLDER_APECHAIN, + destinationAddress: RANDOM_APE_ETH_HOLDER_APECHAIN, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + new ExpectedEvent[](0), + true // This is a fee-on-transfer token, + ); + + vm.stopPrank(); + } + + /// @notice User-funded single-hop swap on Algebra (forward direction). + /// @dev Transfers funds from whale to user and executes swap to same user; enables fee on transfer support. + function test_CanSwap() public override { + vm.startPrank(RANDOM_APE_ETH_HOLDER_APECHAIN); + + // Transfer tokens from whale to USER_SENDER + uint256 amountToTransfer = _getDefaultAmountForTokenIn(); + IERC20(tokenIn).transfer(USER_SENDER, amountToTransfer); + + vm.stopPrank(); + + vm.startPrank(USER_SENDER); + + _testSwap( + AlgebraSwapTestParams({ + from: USER_SENDER, + destinationAddress: USER_SENDER, + tokenIn: address(tokenIn), + amountIn: _getDefaultAmountForTokenIn(), + tokenOut: address(tokenOut), + direction: SwapDirection.Token0ToToken1, + supportsFeeOnTransfer: true + }) + ); + + vm.stopPrank(); + } + + /// @notice Reverse-direction swap roundtrip test to ensure pool works both ways. + function test_CanSwap_Reverse() public { + test_CanSwap(); + + uint256 amountIn = IERC20(address(tokenOut)).balanceOf(USER_SENDER); + + vm.startPrank(USER_SENDER); + + _testSwap( + AlgebraSwapTestParams({ + from: USER_SENDER, + destinationAddress: USER_SENDER, + tokenIn: address(tokenOut), + amountIn: amountIn, + tokenOut: address(tokenIn), + direction: SwapDirection.Token1ToToken0, + supportsFeeOnTransfer: false + }) + ); + + vm.stopPrank(); + } + + /// @notice Multi-hop user->aggregator swap with fee-on-transfer on middle token. + /// @dev Constructs two pools locally, adds liquidity, and executes: + /// - Hop1: user A->B to aggregator + /// - Hop2: aggregator B->C to user + function test_CanSwap_MultiHop_WithFeeOnTransferToken() public { + MultiHopTestState memory state; + state.isFeeOnTransfer = true; + + // Setup tokens and pools + state = _setupTokensAndPools(state); + + // Execute and verify swap + _executeAndVerifyMultiHopSwap(state); + } + + /// @notice Multi-hop user->aggregator swap with regular mid token (no fee). + function test_CanSwap_MultiHop() public override { + MultiHopTestState memory state; + state.isFeeOnTransfer = false; + + // Setup tokens and pools + state = _setupTokensAndPools(state); + + // Execute and verify swap + _executeAndVerifyMultiHopSwap(state); + } + + /// @notice Ensures swap reverts if Algebra callback is never invoked by the pool. + /// @dev Deploys a no-callback mock pool and verifies `SwapCallbackNotExecuted`. + function testRevert_SwapWithoutCallback() public override { + // Pool that does not call back for Algebra + address mockPool = _deployNoCallbackPool(); // your Algebra-specific or shared mock + + // Fund user from whale instead of deal() + vm.prank(RANDOM_APE_ETH_HOLDER_APECHAIN); + IERC20(address(tokenIn)).transfer( + USER_SENDER, + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildCallbackSwapData(mockPool, USER_SENDER); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + SwapCallbackNotExecuted.selector + ); + + vm.stopPrank(); + } + + /// @notice Validates revert when pool is zero address in swap data. + function testRevert_AlgebraSwap_ZeroAddressPool() public { + // Transfer tokens from whale to user + vm.prank(RANDOM_APE_ETH_HOLDER_APECHAIN); + IERC20(tokenIn).transfer(USER_SENDER, 1 * 1e18); + + vm.startPrank(USER_SENDER); + + // Mock token0() call on address(0) + vm.mockCall( + address(0), + abi.encodeWithSelector(IAlgebraPool.token0.selector), + abi.encode(tokenIn) + ); + + // Build route with address(0) as pool + bytes memory swapData = _buildAlgebraSwapData( + AlgebraRouteParams({ + commandCode: CommandType.DistributeUserERC20, + tokenIn: address(tokenIn), + destinationAddress: USER_SENDER, + pool: address(0), // Zero address pool + supportsFeeOnTransfer: true + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + vm.clearMockedCalls(); + } + + // ==== Overrides ==== + + /// @notice Returns Algebra-specific callback selector for the base callback tests. + function _getCallbackSelector() internal view override returns (bytes4) { + return algebraFacet.algebraSwapCallback.selector; + } + + /// @notice Hook: build Algebra swap data [pool, direction(uint8), destinationAddress, supportsFeeOnTransfer(uint8)] + /// @param pool Pool to be used for callback arming tests. + /// @param destinationAddress Destination address. + /// @return swapData Packed bytes starting with `swapAlgebra` selector. + function _buildCallbackSwapData( + address pool, + address destinationAddress + ) internal pure override returns (bytes memory) { + return + abi.encodePacked( + AlgebraFacet.swapAlgebra.selector, + pool, + uint8(1), // Token0->Token1; only the callback arming/clearing is under test + destinationAddress, + uint8(0) // no fee-on-transfer + ); + } + + /// @notice Validates revert when destinationAddress is zero address in swap data. + function testRevert_AlgebraSwap_ZeroAddressDestinationAddress() public { + // Transfer tokens from whale to user + vm.prank(RANDOM_APE_ETH_HOLDER_APECHAIN); + IERC20(tokenIn).transfer(USER_SENDER, 1 * 1e18); + + vm.startPrank(USER_SENDER); + + // Mock token0() call on the pool + vm.mockCall( + poolInOut, + abi.encodeWithSelector(IAlgebraPool.token0.selector), + abi.encode(tokenIn) + ); + + // Build route with address(0) as destination address + bytes memory swapData = _buildAlgebraSwapData( + AlgebraRouteParams({ + commandCode: CommandType.DistributeUserERC20, + tokenIn: address(tokenIn), + destinationAddress: address(0), // Zero address destination address + pool: poolInOut, + supportsFeeOnTransfer: true + }) + ); + + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }); + + _buildRouteAndExecuteAndVerifySwap( + params, + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + vm.clearMockedCalls(); + } + + // ==== Helper Functions ==== + + /// @notice Creates 2 local Algebra pools and seeds liquidity for multi-hop tests. + /// @param state Preallocated struct; `isFeeOnTransfer` toggles mid token behavior. + /// @return Updated state with tokens/pools and funding applied. + function _setupTokensAndPools( + MultiHopTestState memory state + ) private returns (MultiHopTestState memory) { + // Create tokens + ERC20 tokenA = new ERC20( + "Token A", + state.isFeeOnTransfer ? "FTA" : "TA", + 18 + ); + IERC20 tokenB; + ERC20 tokenC = new ERC20( + "Token C", + state.isFeeOnTransfer ? "FTC" : "TC", + 18 + ); + + if (state.isFeeOnTransfer) { + tokenB = IERC20( + address( + new MockFeeOnTransferToken("Fee Token B", "FTB", 18, 300) + ) + ); + } else { + tokenB = IERC20(address(new ERC20("Token B", "TB", 18))); + } + + state.tokenA = IERC20(address(tokenA)); + state.tokenB = tokenB; + state.tokenC = IERC20(address(tokenC)); + + // Label addresses + vm.label(address(state.tokenA), "Token A"); + vm.label(address(state.tokenB), "Token B"); + vm.label(address(state.tokenC), "Token C"); + + // Mint initial token supplies + tokenA.mint(address(this), 1_000_000 * 1e18); + if (!state.isFeeOnTransfer) { + ERC20(address(tokenB)).mint(address(this), 1_000_000 * 1e18); + } else { + MockFeeOnTransferToken(address(tokenB)).mint( + address(this), + 1_000_000 * 1e18 + ); + } + tokenC.mint(address(this), 1_000_000 * 1e18); + + // Create pools + state.pool1 = _createAlgebraPool( + address(state.tokenA), + address(state.tokenB) + ); + state.pool2 = _createAlgebraPool( + address(state.tokenB), + address(state.tokenC) + ); + + vm.label(state.pool1, "Pool 1"); + vm.label(state.pool2, "Pool 2"); + + // Add liquidity + _addLiquidityToPool( + state.pool1, + address(state.tokenA), + address(state.tokenB) + ); + _addLiquidityToPool( + state.pool2, + address(state.tokenB), + address(state.tokenC) + ); + + state.amountToTransfer = 100 * 1e18; + state.amountIn = 50 * 1e18; + + // Transfer tokens to USER_SENDER + IERC20(address(state.tokenA)).transfer( + USER_SENDER, + state.amountToTransfer + ); + + return state; + } + + /// @notice Executes a two-hop route A->B (user) then B->C (aggregator). + /// @param state Prepared state from `_setupTokensAndPools`. + function _executeAndVerifyMultiHopSwap( + MultiHopTestState memory state + ) private { + vm.startPrank(USER_SENDER); + + // Build route and execute swap + SwapTestParams[] memory swapParams = new SwapTestParams[](2); + bytes[] memory swapData = new bytes[](2); + + // First hop: TokenA -> TokenB + swapParams[0] = SwapTestParams({ + tokenIn: address(state.tokenA), + tokenOut: address(state.tokenB), + amountIn: state.amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: address(ldaDiamond), // Send to aggregator for next hop + commandType: CommandType.DistributeUserERC20 + }); + + // Build first hop swap data + swapData[0] = _buildAlgebraSwapData( + AlgebraRouteParams({ + commandCode: CommandType.DistributeUserERC20, + tokenIn: address(state.tokenA), + destinationAddress: address(ldaDiamond), + pool: state.pool1, + supportsFeeOnTransfer: false + }) + ); + + // Second hop: TokenB -> TokenC + swapParams[1] = SwapTestParams({ + tokenIn: address(state.tokenB), + tokenOut: address(state.tokenC), + amountIn: 0, // Not used for DistributeSelfERC20 + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }); + + // Build second hop swap data + swapData[1] = _buildAlgebraSwapData( + AlgebraRouteParams({ + commandCode: CommandType.DistributeSelfERC20, + tokenIn: address(state.tokenB), + destinationAddress: USER_SENDER, + pool: state.pool2, + supportsFeeOnTransfer: state.isFeeOnTransfer + }) + ); + + bytes memory route = _buildMultiHopRoute(swapParams, swapData); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(state.tokenA), + tokenOut: address(state.tokenC), + amountIn: state.amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route + ); + + vm.stopPrank(); + } + + /// @notice Creates an Algebra pool via on-chain factory (fork path). + /// @param tokenA First token. + /// @param tokenB Second token. + /// @return pool New pool address. + function _createAlgebraPool( + address tokenA, + address tokenB + ) internal returns (address pool) { + // Call the actual Algebra factory to create a pool + pool = IAlgebraFactory(ALGEBRA_FACTORY_APECHAIN).createPool( + tokenA, + tokenB + ); + return pool; + } + + /// @notice Adds liquidity to the given Algebra pool using available token balances. + /// @param pool Pool to seed. + /// @param token0 token0 address. + /// @param token1 token1 address. + /// @dev Uses a helper that safely tolerates fee-on-transfer behavior. + function _addLiquidityToPool( + address pool, + address token0, + address token1 + ) internal { + // For fee-on-transfer tokens, we need to send more to account for the fee + // We'll use a small amount and send extra to cover fees + uint256 initialAmount0 = 1e17; // 0.1 token + uint256 initialAmount1 = 1e17; // 0.1 token + + // Send extra for fee-on-transfer tokens (10% extra should be enough for our test tokens with 5% fee) + uint256 transferAmount0 = (initialAmount0 * 110) / 100; + uint256 transferAmount1 = (initialAmount1 * 110) / 100; + + // Initialize with 1:1 price ratio (Q64.96 format) + uint160 initialPrice = uint160(1 << 96); + IAlgebraPool(pool).initialize(initialPrice); + + // Create AlgebraLiquidityAdderHelper with safe transfer logic + AlgebraLiquidityAdderHelper algebraLiquidityAdderHelper = new AlgebraLiquidityAdderHelper( + token0, + token1 + ); + + // Transfer tokens with extra amounts to account for fees + IERC20(token0).transfer( + address(algebraLiquidityAdderHelper), + transferAmount0 + ); + IERC20(token1).transfer( + address(algebraLiquidityAdderHelper), + transferAmount1 + ); + + // Get actual balances to use for liquidity, accounting for any fees + uint256 actualBalance0 = IERC20(token0).balanceOf( + address(algebraLiquidityAdderHelper) + ); + uint256 actualBalance1 = IERC20(token1).balanceOf( + address(algebraLiquidityAdderHelper) + ); + + // Use the smaller of the two balances for liquidity amount + uint128 liquidityAmount = uint128( + actualBalance0 < actualBalance1 ? actualBalance0 : actualBalance1 + ); + + // Add liquidity using the actual token amounts we have + algebraLiquidityAdderHelper.addLiquidity( + pool, + -887220, + 887220, + liquidityAmount / 2 // Use half of available liquidity to ensure success + ); + } + + /// @notice Builds Algebra swap data payload for a hop. + /// @param params Route parameters used to determine direction and toggle fee on transfer token support. + /// @return Packed bytes for `swapAlgebra`. + function _buildAlgebraSwapData( + AlgebraRouteParams memory params + ) private view returns (bytes memory) { + address token0 = IAlgebraPool(params.pool).token0(); + bool zeroForOne = (params.tokenIn == token0); + SwapDirection direction = zeroForOne + ? SwapDirection.Token0ToToken1 + : SwapDirection.Token1ToToken0; + + // This data blob is what the AlgebraFacet will receive and parse + return + abi.encodePacked( + AlgebraFacet.swapAlgebra.selector, + params.pool, + uint8(direction), + params.destinationAddress, + params.supportsFeeOnTransfer ? uint8(1) : uint8(0) + ); + } + + /// @notice High-level helper that performs a fork-based Algebra swap and verifies balances. + /// @param params See `AlgebraSwapTestParams`. + /// @dev Uses QuoterV2 to get expectedOut and sets minOut = expected - 1 for tolerance. + function _testSwap(AlgebraSwapTestParams memory params) internal { + // Find or create a pool + address pool = _getPool(params.tokenIn, params.tokenOut); + + // Get expected output from QuoterV2 + uint256 expectedOutput = _getQuoteExactInput( + params.tokenIn, + params.tokenOut, + params.amountIn + ); + + // Add 1 wei slippage buffer + uint256 minOutput = expectedOutput > 0 ? expectedOutput - 1 : 0; + + // if tokens come from the aggregator (address(ldaDiamond)), use command code 1; otherwise, use 2. + CommandType commandCode = params.from == address(ldaDiamond) + ? CommandType.DistributeSelfERC20 + : CommandType.DistributeUserERC20; + + // Pack the specific data for this swap + bytes memory swapData = _buildAlgebraSwapData( + AlgebraRouteParams({ + commandCode: commandCode, + tokenIn: params.tokenIn, + destinationAddress: params.destinationAddress, + pool: pool, + supportsFeeOnTransfer: params.supportsFeeOnTransfer + }) + ); + + // Build route with minOutput that includes slippage buffer + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: params.amountIn, + minOut: minOutput, + sender: params.from, + destinationAddress: params.destinationAddress, + commandType: commandCode + }), + swapData + ); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: params.amountIn, + minOut: minOutput, + sender: params.from, + destinationAddress: params.destinationAddress, + commandType: params.from == address(ldaDiamond) + ? CommandType.DistributeSelfERC20 + : CommandType.DistributeUserERC20 + }), + route, + new ExpectedEvent[](0), + params.supportsFeeOnTransfer + ); + } + + /// @notice Resolves pool by pair via router/factory and reverts if missing. + /// @param tokenA First token. + /// @param tokenB Second token. + /// @return pool Pool address. + function _getPool( + address tokenA, + address tokenB + ) private view returns (address pool) { + pool = IAlgebraRouter(ALGEBRA_FACTORY_APECHAIN).poolByPair( + tokenA, + tokenB + ); + if (pool == address(0)) revert PoolDoesNotExist(); + return pool; + } + + /// @notice Quotes expected output using Algebra QuoterV2 for exactInputSingle. + /// @param tokenIn Input token. + /// @param tokenOut Output token. + /// @param amountIn Input amount. + /// @return amountOut Quote for amount out. + function _getQuoteExactInput( + address tokenIn, + address tokenOut, + uint256 amountIn + ) private returns (uint256 amountOut) { + (amountOut, ) = IAlgebraQuoter(ALGEBRA_QUOTER_V2_APECHAIN) + .quoteExactInputSingle(tokenIn, tokenOut, amountIn, 0); + return amountOut; + } +} + +/// @title AlgebraLiquidityAdderHelper +/// @notice Adds liquidity to an Algebra pool using balances available on this helper. +/// @dev Implements `algebraMintCallback` to transfer owed amounts to the pool. +contract AlgebraLiquidityAdderHelper { + error Token0TransferFailed(); + error Token1TransferFailed(); + + /// @notice token0 used by the target pool. + address public immutable TOKEN_0; + /// @notice token1 used by the target pool. + address public immutable TOKEN_1; + + /// @notice Sets immutable token references. + /// @param _token0 token0 address. + /// @param _token1 token1 address. + constructor(address _token0, address _token1) { + TOKEN_0 = _token0; + TOKEN_1 = _token1; + } + + // ==== External Functions ==== + + /// @notice Adds liquidity by calling Algebra pool `mint` and returning actual spend. + /// @param pool Pool address. + /// @param bottomTick Lower tick. + /// @param topTick Upper tick. + /// @param amount Desired liquidity amount. + /// @return amount0 Actual token0 spent. + /// @return amount1 Actual token1 spent. + /// @return liquidityActual Actual liquidity minted. + function addLiquidity( + address pool, + int24 bottomTick, + int24 topTick, + uint128 amount + ) + external + returns (uint256 amount0, uint256 amount1, uint128 liquidityActual) + { + // Get balances before + uint256 balance0Before = IERC20(TOKEN_0).balanceOf(address(this)); + uint256 balance1Before = IERC20(TOKEN_1).balanceOf(address(this)); + + // Call mint + (amount0, amount1, liquidityActual) = IAlgebraPool(pool).mint( + address(this), + address(this), + bottomTick, + topTick, + amount, + abi.encode(TOKEN_0, TOKEN_1) + ); + + // Get balances after to account for fees + uint256 balance0After = IERC20(TOKEN_0).balanceOf(address(this)); + uint256 balance1After = IERC20(TOKEN_1).balanceOf(address(this)); + + // Calculate actual amounts transferred accounting for fees + amount0 = balance0Before - balance0After; + amount1 = balance1Before - balance1After; + + return (amount0, amount1, liquidityActual); + } + + /// @notice Callback invoked by Algebra pool to collect owed tokens during mint. + /// @param amount0Owed Owed token0. + /// @param amount1Owed Owed token1. + /// @dev Transfers up to current balance for each owed amount and ensures non-zero effect. + function algebraMintCallback( + uint256 amount0Owed, + uint256 amount1Owed, + bytes calldata + ) external { + // Check token balances + uint256 balance0 = IERC20(TOKEN_0).balanceOf(address(this)); + uint256 balance1 = IERC20(TOKEN_1).balanceOf(address(this)); + + // Transfer what we can, limited by actual balance + if (amount0Owed > 0) { + uint256 amount0ToSend = amount0Owed > balance0 + ? balance0 + : amount0Owed; + uint256 balance0Before = IERC20(TOKEN_0).balanceOf( + address(msg.sender) + ); + IERC20(TOKEN_0).transfer(msg.sender, amount0ToSend); + uint256 balance0After = IERC20(TOKEN_0).balanceOf( + address(msg.sender) + ); + if (balance0After <= balance0Before) revert Token0TransferFailed(); + } + + if (amount1Owed > 0) { + uint256 amount1ToSend = amount1Owed > balance1 + ? balance1 + : amount1Owed; + uint256 balance1Before = IERC20(TOKEN_1).balanceOf( + address(msg.sender) + ); + IERC20(TOKEN_1).transfer(msg.sender, amount1ToSend); + uint256 balance1After = IERC20(TOKEN_1).balanceOf( + address(msg.sender) + ); + if (balance1After <= balance1Before) revert Token1TransferFailed(); + } + } +} diff --git a/test/solidity/Periphery/LDA/Facets/CoreRouteFacet.t.sol b/test/solidity/Periphery/LDA/Facets/CoreRouteFacet.t.sol new file mode 100644 index 000000000..2eae7749e --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/CoreRouteFacet.t.sol @@ -0,0 +1,674 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { ERC20PermitMock } from "lib/Permit2/lib/openzeppelin-contracts/contracts/mocks/ERC20PermitMock.sol"; +import { CoreRouteFacet } from "lifi/Periphery/LDA/Facets/CoreRouteFacet.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; +import { BaseCoreRouteTest } from "../BaseCoreRoute.t.sol"; +import { Vm } from "forge-std/Vm.sol"; + +/// @title CoreRouteFacetTest +/// @notice Tests the CoreRouteFacet's command parser, permit handling, and minimal invariants. +/// @dev Adds small mock “facets” that emulate pull/native handlers for exercising route execution. +contract CoreRouteFacetTest is BaseCoreRouteTest { + using SafeTransferLib for address; + + /// @notice Cached selector for the mock pull facet to simplify route building in tests. + bytes4 internal pullSel; + + // ==== Events ==== + event Pulled(uint256 amt); + + // ==== Setup Functions ==== + + /// @notice Registers a mock pull facet once and stores its selector for reuse. + /// @dev Also calls parent `setUp` to add CoreRouteFacet to the diamond. + function setUp() public override { + super.setUp(); + // Register mock pull facet once and store selector + MockPullERC20Facet mockPull = new MockPullERC20Facet(); + bytes4[] memory sel = new bytes4[](1); + sel[0] = MockPullERC20Facet.pull.selector; + addFacet(address(ldaDiamond), address(mockPull), sel); + pullSel = sel[0]; + } + + // ==== Helper Functions ==== + + /// @notice Adds a mock native handler facet to the diamond for ProcessNative tests. + function _addMockNativeFacet() internal { + MockNativeFacet mock = new MockNativeFacet(); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockNativeFacet.handleNative.selector; + addFacet(address(ldaDiamond), address(mock), selectors); + } + + /// @notice Adds a mock pull facet to the diamond and returns its selector. + /// @return sel Selector of the mock pull function. + function _addMockPullFacet() internal returns (bytes4 sel) { + MockPullERC20Facet mock = new MockPullERC20Facet(); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockPullERC20Facet.pull.selector; + addFacet(address(ldaDiamond), address(mock), selectors); + return selectors[0]; + } + + /// @notice Signs an EIP-2612 permit for a mock token and returns the ECDSA tuple. + /// @param token Permit-enabled mock token. + /// @param ownerPk Private key of the owner (forge-anvil test key). + /// @param owner Owner address. + /// @param spender Spender to approve (diamond in our tests). + /// @param value Allowance value. + /// @param deadline Permit deadline. + /// @return v ECDSA v + /// @return r ECDSA r + /// @return s ECDSA s + function _signPermit( + ERC20PermitMock token, + uint256 ownerPk, + address owner, + address spender, + uint256 value, + uint256 deadline + ) internal view returns (uint8 v, bytes32 r, bytes32 s) { + bytes32 typehash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + bytes32 structHash = keccak256( + abi.encode( + typehash, + owner, + spender, + value, + token.nonces(owner), + deadline + ) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash) + ); + (v, r, s) = vm.sign(ownerPk, digest); + } + + // ==== Test Cases ==== + + /// @notice Verifies DistributeNative command passes ETH to receiver and emits Route with exact out. + /// @dev Builds a route with a mock native handler and funds USER_SENDER with 1 ETH. + function test_DistributeNativeCommandSendsEthToReceiver() public { + _addMockNativeFacet(); + + uint256 amount = 1 ether; + + // Fund the actual caller (USER_SENDER) + vm.deal(USER_SENDER, amount); + + // swapData: selector + abi.encode(USER_RECEIVER) + bytes memory swapData = abi.encodePacked( + MockNativeFacet.handleNative.selector, + abi.encode(USER_RECEIVER) + ); + + // route: [3][num=1][share=FULL_SHARE][len][swapData] + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(0), + tokenOut: address(0), + amountIn: amount, + minOut: 0, + sender: USER_SENDER, // Use USER_SENDER directly + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeNative + }); + + bytes memory route = _buildBaseRoute(params, swapData); + + vm.prank(USER_SENDER); // Set msg.sender to USER_SENDER + _executeAndVerifySwap( + params, + route, + new ExpectedEvent[](0), + false, + RouteEventVerification({ + expectedExactOut: amount, + checkData: true + }) + ); + } + + /// @notice Applies an EIP-2612 permit via ApplyPermit command and verifies allowance on diamond. + function test_ApplyPermitCommandSetsAllowanceOnDiamond() public { + uint256 ownerPk = 0xA11CE; + address owner = vm.addr(ownerPk); + uint256 init = 1_000_000e18; + ERC20PermitMock token = new ERC20PermitMock( + "Mock", + "MCK", + owner, + init + ); + + uint256 value = 500_000e18; + uint256 deadline = block.timestamp + 1 days; + + (uint8 v, bytes32 r, bytes32 s) = _signPermit( + token, + ownerPk, + owner, + address(ldaDiamond), + value, + deadline + ); + + // route: [5][value][deadline][v][r][s] + bytes memory route = abi.encodePacked( + uint8(5), + value, + deadline, + v, + r, + s + ); + + vm.prank(owner); + coreRouteFacet.processRoute( + address(token), // tokenIn used by _applyPermit + 0, + address(token), // tokenOut unused; must be a contract for balanceOf read + 0, + owner, + route + ); + + assertEq( + IERC20(address(token)).allowance(owner, address(ldaDiamond)), + value, + "permit allowance not set" + ); + } + + /// @notice Unknown command codes should revert; verifies UnknownCommandCode error. + function testRevert_WhenCommandCodeIsUnknown() public { + bytes memory route = abi.encodePacked(uint8(9)); + + vm.prank(USER_SENDER); + vm.expectRevert(CoreRouteFacet.UnknownCommandCode.selector); + coreRouteFacet.processRoute( + address(0), + 0, + address(0), + 0, + USER_RECEIVER, + route + ); + } + + /// @notice Unknown selectors in a step should revert; verifies UnknownSelector. + function testRevert_WhenSelectorIsUnknown() public { + ERC20PermitMock token = new ERC20PermitMock( + "Mock2", + "MCK2", + USER_SENDER, + 1e18 + ); + + bytes memory swapData = abi.encodePacked(bytes4(0xdeadbeef)); + + // DistributeUserERC20: [2][tokenIn][num=1][share=FULL_SHARE][len=4][selector=0xdeadbeef] + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(token), + tokenOut: address(token), + amountIn: 0, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.prank(USER_SENDER); + vm.expectRevert(CoreRouteFacet.UnknownSelector.selector); + coreRouteFacet.processRoute( + address(token), + 0, + address(token), + 0, + USER_RECEIVER, + route + ); + } + + /// @notice TokenInSpendingExceeded: trigger by charging the user twice via two DistributeUserERC20 steps. + function testRevert_WhenInputBalanceIsInsufficientForTwoSteps() public { + // Prepare token and approvals + uint256 amountIn = 1e18; + ERC20PermitMock token = new ERC20PermitMock( + "Pull", + "PULL", + USER_SENDER, + 2 * amountIn + ); + + vm.startPrank(USER_SENDER); + IERC20(address(token)).approve(address(ldaDiamond), type(uint256).max); + + bytes memory swapData = abi.encodePacked(pullSel); + + // Build one step: [2][tokenIn][num=1][share=FULL_SHARE][len=4][sel] + bytes memory step = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(token), + tokenOut: address(0), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + bytes memory route = bytes.concat(step, step); + + vm.expectRevert( + abi.encodeWithSelector( + CoreRouteFacet.SwapTokenInSpendingExceeded.selector, + amountIn, // available = final(0) + amountIn + 2 * amountIn // required = initial (we minted 2*amountIn) + ) + ); + coreRouteFacet.processRoute( + address(token), // tokenIn for balance checks + amountIn, // only 1e18 declared + address(0), // no tokenOut change + 0, + USER_RECEIVER, + route + ); + vm.stopPrank(); + } + + /// @notice Same as above but with tokenOut set to an ERC20, to ensure path-independent behavior. + function testRevert_WhenInputBalanceIsInsufficientForTwoStepsWithERC20Out() + public + { + uint256 amountIn = 1e18; + ERC20PermitMock token = new ERC20PermitMock( + "Pull2", + "PULL2", + USER_SENDER, + 2 * amountIn + ); + ERC20PermitMock tokenOut = new ERC20PermitMock( + "Out", + "OUT", + address(this), + 0 + ); + + vm.startPrank(USER_SENDER); + IERC20(address(token)).approve(address(ldaDiamond), type(uint256).max); + + bytes memory swapData = abi.encodePacked(pullSel); + + bytes memory step = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(token), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + bytes memory route = bytes.concat(step, step); + + vm.expectRevert( + abi.encodeWithSelector( + CoreRouteFacet.SwapTokenInSpendingExceeded.selector, + amountIn, // available = final(0) + amountIn + 2 * amountIn // required = initial (we minted 2*amountIn) + ) + ); + coreRouteFacet.processRoute( + address(token), + amountIn, + address(tokenOut), + 0, + USER_RECEIVER, + route + ); + vm.stopPrank(); + } + + function testRevert_WhenOutputBalanceIsZeroForERC20() public { + // Register the mock pull facet; it pulls tokenIn but never transfers tokenOut to `to` + bytes4 sel = pullSel; + + uint256 amountIn = 1e18; + ERC20PermitMock tokenIn = new ERC20PermitMock( + "IN", + "IN", + USER_SENDER, + amountIn + ); + ERC20PermitMock tokenOut = new ERC20PermitMock( + "OUT", + "OUT", + USER_RECEIVER, + 0 + ); // destination address starts at 0 + + bytes memory swapData = abi.encodePacked(sel); + + // Build one DistributeUserERC20 step: [2][tokenIn][num=1][share=FULL_SHARE][len=4][sel] + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 1, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); // single step; no tokenOut will be sent to receiver + + vm.startPrank(USER_SENDER); + IERC20(address(tokenIn)).approve(address(ldaDiamond), amountIn); + + // Expect MinimalOutputBalanceViolation with deltaOut = 0 + vm.expectRevert( + abi.encodeWithSelector( + CoreRouteFacet.SwapTokenOutAmountTooLow.selector, + uint256(0) + ) + ); + + coreRouteFacet.processRoute( + address(tokenIn), + amountIn, + address(tokenOut), // tokenOut is ERC20 + 1, // amountOutMin > 0 to trigger the revert when no output arrives + USER_RECEIVER, + route + ); + vm.stopPrank(); + } + + function testRevert_WhenOutputBalanceIsZeroForNative() public { + // Register the mock pull facet; it pulls tokenIn but never transfers native to `to` + bytes4 sel = pullSel; + + uint256 amountIn = 1e18; + ERC20PermitMock tokenIn = new ERC20PermitMock( + "IN2", + "IN2", + USER_SENDER, + amountIn + ); + + bytes memory swapData = abi.encodePacked(sel); + + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(0), + amountIn: amountIn, + minOut: 1, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.startPrank(USER_SENDER); + IERC20(address(tokenIn)).approve(address(ldaDiamond), amountIn); + + // Expect MinimalOutputBalanceViolation with deltaOut = 0 (no ETH sent) + vm.expectRevert( + abi.encodeWithSelector( + CoreRouteFacet.SwapTokenOutAmountTooLow.selector, + uint256(0) + ) + ); + + coreRouteFacet.processRoute( + address(tokenIn), + amountIn, + address(0), // tokenOut is native + 1, // amountOutMin > 0 + USER_RECEIVER, + route + ); + vm.stopPrank(); + } + + function test_DistributeUserERC20_TwoLegs_50_50_UsesOriginalTotalAndRemainder() + public + { + // Deploy + register mock facet that records each leg's amount via an event + MockRecordPullFacet mock = new MockRecordPullFacet(); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockRecordPullFacet.pullAndRecord.selector; + addFacet(address(ldaDiamond), address(mock), selectors); + + // Mint token to USER_SENDER + uint256 totalAmount = 1e18; + ERC20PermitMock token = new ERC20PermitMock( + "IN", + "IN", + USER_SENDER, + totalAmount + ); + + // Build route: DistributeUserERC20 with 2 legs, 50%/50% + uint16 shareScale = type(uint16).max; + uint16 halfShare = shareScale / 2; + bytes memory legCalldata = abi.encodePacked(selectors[0]); // 4 bytes + bytes memory route = abi.encodePacked( + uint8(2), // DistributeUserERC20 + address(token), // tokenIn + uint8(2), // n = 2 legs + halfShare, + uint16(4), + legCalldata, // leg1: 50% + halfShare, + uint16(4), + legCalldata // leg2: 50% + ); + + vm.startPrank(USER_SENDER); + IERC20(address(token)).approve(address(ldaDiamond), totalAmount); + + // Record logs and run + vm.recordLogs(); + coreRouteFacet.processRoute( + address(token), // tokenIn + totalAmount, // declared amount + address(0), // tokenOut (unused) + 0, + USER_RECEIVER, + route + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + vm.stopPrank(); + + // Collect Pulled(uint256) amounts + bytes32 pulledEventTopic = keccak256("Pulled(uint256)"); + uint256[] memory pulledAmounts = new uint256[](2); + uint256 eventCount = 0; + for (uint256 i = 0; i < logs.length; i++) { + if ( + logs[i].topics.length > 0 && + logs[i].topics[0] == pulledEventTopic + ) { + pulledAmounts[eventCount++] = uint256(bytes32(logs[i].data)); + } + } + assertEq(eventCount, 2, "expected two leg pulls"); + + // Expected amounts: first computed from original total, last is exact remainder + uint256 expectedFirstLeg = (totalAmount * halfShare) / shareScale; + uint256 expectedSecondLeg = totalAmount - expectedFirstLeg; + + assertEq(pulledAmounts[0], expectedFirstLeg, "leg1 amount mismatch"); + assertEq( + pulledAmounts[1], + expectedSecondLeg, + "leg2 remainder mismatch" + ); + assertEq( + pulledAmounts[0] + pulledAmounts[1], + totalAmount, + "sum != total" + ); + } + + function test_DistributeUserERC20_ThreeLegs_25_25_50_UsesOriginalTotalAndRemainder() + public + { + // Deploy + register mock facet + MockRecordPullFacet mock = new MockRecordPullFacet(); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = MockRecordPullFacet.pullAndRecord.selector; + addFacet(address(ldaDiamond), address(mock), selectors); + + uint256 totalAmount = 1e18; + ERC20PermitMock token = new ERC20PermitMock( + "IN2", + "IN2", + USER_SENDER, + totalAmount + ); + + uint16 shareScale = type(uint16).max; + uint16 quarterShare = shareScale / 4; + // Third leg share is arbitrary; last leg receives remainder by design + uint16 dummyShare = shareScale / 2; + + bytes memory legCalldata = abi.encodePacked(selectors[0]); // 4 bytes + bytes memory route = abi.encodePacked( + uint8(2), // DistributeUserERC20 + address(token), // tokenIn + uint8(3), // n = 3 legs + quarterShare, + uint16(4), + legCalldata, // 25% + quarterShare, + uint16(4), + legCalldata, // 25% + dummyShare, + uint16(4), + legCalldata // (ignored) gets remainder + ); + + vm.startPrank(USER_SENDER); + IERC20(address(token)).approve(address(ldaDiamond), totalAmount); + vm.recordLogs(); + coreRouteFacet.processRoute( + address(token), + totalAmount, + address(0), + 0, + USER_RECEIVER, + route + ); + Vm.Log[] memory logs = vm.getRecordedLogs(); + vm.stopPrank(); + + bytes32 pulledEventTopic = keccak256("Pulled(uint256)"); + uint256[] memory pulledAmounts = new uint256[](3); + uint256 eventCount = 0; + for (uint256 i = 0; i < logs.length; i++) { + if ( + logs[i].topics.length > 0 && + logs[i].topics[0] == pulledEventTopic + ) { + pulledAmounts[eventCount++] = uint256(bytes32(logs[i].data)); + } + } + assertEq(eventCount, 3, "expected three leg pulls"); + + uint256 expectedFirstLeg = (totalAmount * quarterShare) / shareScale; + uint256 expectedSecondLeg = (totalAmount * quarterShare) / shareScale; + uint256 expectedThirdLeg = totalAmount - + (expectedFirstLeg + expectedSecondLeg); + + assertEq(pulledAmounts[0], expectedFirstLeg, "leg1 amount mismatch"); + assertEq(pulledAmounts[1], expectedSecondLeg, "leg2 amount mismatch"); + assertEq( + pulledAmounts[2], + expectedThirdLeg, + "leg3 remainder mismatch" + ); + assertEq( + pulledAmounts[0] + pulledAmounts[1] + pulledAmounts[2], + totalAmount, + "sum != total" + ); + } +} + +/// @dev Mock facet that records each leg's amount via an event +contract MockRecordPullFacet { + event Pulled(uint256 amt); + function pullAndRecord( + bytes memory /*payload*/, + address from, + address tokenIn, + uint256 amountIn + ) external returns (uint256) { + if (from == msg.sender) { + LibAsset.transferFromERC20( + tokenIn, + msg.sender, + address(this), + amountIn + ); + } + emit Pulled(amountIn); + return amountIn; + } +} + +/// @dev Mock facet implementing LDA's standard interface for testing ERC20 token pulls +contract MockPullERC20Facet { + function pull( + bytes memory /*payload*/, + address from, + address tokenIn, + uint256 amountIn + ) external returns (uint256) { + if (from == msg.sender) { + LibAsset.transferFromERC20( + tokenIn, + msg.sender, + address(this), + amountIn + ); + } + return amountIn; + } +} + +/// @dev Mock facet implementing LDA's standard interface for testing native token handling +contract MockNativeFacet { + function handleNative( + bytes memory payload, + address /*from*/, + address /*tokenIn*/, + uint256 amountIn + ) external payable returns (uint256) { + address receiverAddress = abi.decode(payload, (address)); + LibAsset.transferAsset(address(0), payable(receiverAddress), amountIn); + return amountIn; + } +} diff --git a/test/solidity/Periphery/LDA/Facets/CurveFacet.t.sol b/test/solidity/Periphery/LDA/Facets/CurveFacet.t.sol new file mode 100644 index 000000000..01d0f4f4b --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/CurveFacet.t.sol @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { BaseDEXFacetTest } from "../BaseDEXFacet.t.sol"; +import { CurveFacet } from "lifi/Periphery/LDA/Facets/CurveFacet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; + +/// @title CurveFacetTest +/// @notice Linea Curve tests via LDA route. +/// @dev Verifies single-hop, aggregator flow, and revert paths. +contract CurveFacetTest is BaseDEXFacetTest { + /// @notice Facet proxy for swaps bound to the diamond after setup. + CurveFacet internal curveFacet; + + /// @notice Additional legacy curve pool for stETH/ETH + address internal poolStETHETH; + /// @notice stETH token for stETH/ETH pool + IERC20 internal stETH; + + /// @notice Selects Linea fork and block height used by tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "mainnet", + blockNumber: 23224347 + }); + } + + /// @notice Deploys CurveFacet and returns its swap selector for diamond cut. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + curveFacet = new CurveFacet(); + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = curveFacet.swapCurve.selector; + return (address(curveFacet), functionSelectors); + } + + /// @notice Sets the facet instance to the diamond proxy after facet cut. + function _setFacetInstance(address payable ldaDiamond) internal override { + curveFacet = CurveFacet(ldaDiamond); + } + + /// @notice Defines tokens and pools used by tests (WETH/USDC/USDT). + function _setupDexEnv() internal override { + tokenIn = IERC20(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E); // crvUSD + tokenMid = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // USDC + tokenOut = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); // USDT + poolInMid = 0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E; // crvUSD-USDC + poolMidOut = 0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85; // USDC-USDT + + // additional tokens for legacy curve pools + poolStETHETH = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; // stETH/ETH pool + stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); // stETH + } + + /// @notice Single‐pool swap: USER sends crvUSD -> receives USDC. + function test_CanSwap() public override { + // Transfer 1 000 crvUSD from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolInMid, + isV2: true, // This is a V2 pool + fromIndex: 1, // Changed: crvUSD is at index 1 + toIndex: 0, // Changed: USDC is at index 0 + destinationAddress: address(USER_SENDER), + tokenOut: address(tokenMid) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + function test_CanSwap_FromDexAggregator() public override { + // Fund the aggregator with 1 000 crvUSD + deal( + address(tokenIn), + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolInMid, + isV2: true, // This is a V2 pool + fromIndex: 1, // Changed: crvUSD is at index 1 + toIndex: 0, // Changed: USDC is at index 0 + destinationAddress: address(USER_SENDER), + tokenOut: address(tokenMid) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn() - 1, // Account for slot-undrain + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + function test_CanSwap_MultiHop() public override { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + // Build swap data for both hops + bytes memory firstSwapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolInMid, + isV2: true, // This is a V2 pool + fromIndex: 1, // Changed: crvUSD is at index 1 + toIndex: 0, // Changed: USDC is at index 0 + destinationAddress: poolMidOut, + tokenOut: address(tokenMid) + }) + ); + + bytes memory secondSwapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolMidOut, + isV2: true, // was false; NG pool uses 5-arg + exchange_received + fromIndex: 0, + toIndex: 1, + destinationAddress: address(USER_SENDER), + tokenOut: address(tokenOut) + }) + ); + + SwapTestParams[] memory params = new SwapTestParams[](2); + bytes[] memory swapData = new bytes[](2); + + params[0] = SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: poolMidOut, + commandType: CommandType.DistributeUserERC20 + }); + swapData[0] = firstSwapData; + + params[1] = SwapTestParams({ + tokenIn: address(tokenMid), + tokenOut: address(tokenOut), + amountIn: 0, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DispatchSinglePoolSwap + }); + swapData[1] = secondSwapData; + + bytes memory route = _buildMultiHopRoute(params, swapData); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route + ); + + vm.stopPrank(); + } + + /// @notice Empty test as Curve does not use callbacks + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + function testRevert_CallbackFromUnexpectedSender() public override { + // Curve does not use callbacks - test intentionally empty + } + + /// @notice Empty test as Curve does not use callbacks + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + function testRevert_SwapWithoutCallback() public override { + // Curve does not use callbacks - test intentionally empty + } + + /// @notice Legacy 3pool swap: USER sends USDC -> receives USDT via 4-arg exchange (isV2=false). + function test_CanSwap_Legacy3Pool_USDC_to_USDT() public { + // 3pool (DAI,USDC,USDT) mainnet + address poolLegacy = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; + uint256 amountIn = 1_000 * 1e6; // 1,000 USDC (6 decimals) + + // Fund user with USDC + deal(address(tokenMid), USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + // Build legacy swap data (isV2=false -> 4-arg exchange) + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolLegacy, + isV2: false, // legacy/main path + fromIndex: 1, // USDC index in 3pool + toIndex: 2, // USDT index in 3pool + destinationAddress: USER_SENDER, + tokenOut: address(tokenOut) // USDT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenMid), // USDC + tokenOut: address(tokenOut), // USDT + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Legacy 3pool swap funded by aggregator: USDC -> USDT via 4-arg exchange (isV2=false). + function test_CanSwap_FromDexAggregator_Legacy3Pool_USDC_to_USDT() public { + address poolLegacy = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; + uint256 amountIn = 1_000 * 1e6; + + // Fund aggregator with USDC + deal(address(tokenMid), address(ldaDiamond), amountIn); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolLegacy, + isV2: false, // legacy/main path + fromIndex: 1, // USDC + toIndex: 2, // USDT + destinationAddress: USER_SENDER, + tokenOut: address(tokenOut) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenMid), // USDC + tokenOut: address(tokenOut), // USDT + amountIn: amountIn - 1, // follow undrain convention + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Tests that V2/NG pools reject native ETH input + /// @dev Verifies the InvalidCallData revert condition when sending ETH to modern pools + function testRevert_NativeETHWithV2Pool() public { + uint256 amountIn = 1 ether; + vm.deal(USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolInMid, // Using a V2 pool + isV2: true, + fromIndex: 0, + toIndex: 1, + destinationAddress: USER_SENDER, + tokenOut: address(tokenIn) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(0), // Native ETH + tokenOut: address(tokenIn), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeNative + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Tests that the facet reverts on zero pool or destination addresses. + /// @dev Verifies the InvalidCallData revert condition in swapCurve. + function testRevert_InvalidPoolOrDestinationAddress() public { + // Fund user with tokens to avoid underflow in approval + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + // --- Test case 1: Zero pool address --- + bytes memory swapDataZeroPool = _buildCurveSwapData( + CurveSwapParams({ + pool: address(0), // Invalid pool + isV2: true, + fromIndex: 1, + toIndex: 0, + destinationAddress: USER_SENDER, + tokenOut: address(tokenMid) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), // Use actual amount since we need valid approvals + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataZeroPool, + InvalidCallData.selector + ); + + // --- Test case 2: Zero destination address --- + bytes memory swapDataZeroDestination = _buildCurveSwapData( + CurveSwapParams({ + pool: poolInMid, + isV2: true, + fromIndex: 1, + toIndex: 0, + destinationAddress: address(0), // Invalid destination + tokenOut: address(tokenMid) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), // Use actual amount since we need valid approvals + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataZeroDestination, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Legacy stETH pool swap: USER sends native ETH -> receives stETH via 4-arg exchange. + function test_CanSwap_LegacyEthPool_ETH_to_stETH() public { + // stETH/ETH pool on mainnet + uint256 amountIn = 1 ether; // 1 native ETH + + // Fund user with ETH + vm.deal(USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + // Build legacy swap data for ETH -> stETH + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolStETHETH, + isV2: false, // legacy path + fromIndex: 0, // ETH index in stETH pool + toIndex: 1, // stETH index in stETH pool + destinationAddress: USER_SENDER, + tokenOut: address(stETH) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(0), // Native ETH + tokenOut: address(stETH), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeNative // Use native ETH distribution + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Legacy stETH pool swap: USER sends stETH -> receives native ETH via 4-arg exchange. + function test_CanSwap_LegacyEthPool_stETH_to_ETH() public { + // stETH/ETH pool on mainnet + uint256 amountIn = 1 ether; // 1 stETH + + address stETHWhaleAddress = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + + // Fund user with stETH + vm.prank(stETHWhaleAddress); + stETH.transfer(USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + // Build legacy swap data for stETH -> ETH + bytes memory swapData = _buildCurveSwapData( + CurveSwapParams({ + pool: poolStETHETH, + isV2: false, // legacy path + fromIndex: 1, // stETH index + toIndex: 0, // ETH index + destinationAddress: USER_SENDER, + tokenOut: address(0) // Native ETH + }) + ); + + // Use the standard helper with isFeeOnTransferToken=true to handle stETH balance differences + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(stETH), + tokenOut: address(0), // Native ETH + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + new ExpectedEvent[](0), + true // Allow for small balance differences due to stETH rebasing (fee-on-transfer behavior) + ); + + vm.stopPrank(); + } + + /// @notice Curve swap parameter shape used for `swapCurve`. + struct CurveSwapParams { + address pool; + bool isV2; + int8 fromIndex; + int8 toIndex; + address destinationAddress; + address tokenOut; + } + + /// @notice Builds Curve swap payload for route steps. + /// @param params pool/to/withdrawMode/isV1Pool/vault tuple. + function _buildCurveSwapData( + CurveSwapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + curveFacet.swapCurve.selector, + params.pool, + params.isV2 ? 1 : 0, + params.fromIndex, + params.toIndex, + params.destinationAddress, + params.tokenOut + ); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/EnosysDexV3Facet.t.sol b/test/solidity/Periphery/LDA/Facets/EnosysDexV3Facet.t.sol new file mode 100644 index 000000000..6f713b4ec --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/EnosysDexV3Facet.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { BaseUniV3StyleDEXFacetTest } from "../BaseUniV3StyleDEXFacet.t.sol"; + +/// @title EnosysDEXV3FacetTest +/// @notice Forked UniV3-style tests for Enosys DEX V3 pools via LDA route. +/// @dev Configures Flare network and a concrete pool pair; inherits execution helpers from the base. +contract EnosysDEXV3FacetTest is BaseUniV3StyleDEXFacetTest { + // ==== Setup Functions ==== + + /// @notice Selects Flare fork and block height used for deterministic tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "flare", + blockNumber: 42652369 + }); + } + + /// @notice Returns Enosys-specific UniV3 swap callback selector. + function _getCallbackSelector() internal pure override returns (bytes4) { + return UniV3StyleFacet.enosysdexV3SwapCallback.selector; + } + + /// @notice Sets tokenIn/out and pool for Enosys V3 USDT0 pair on Flare. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x140D8d3649Ec605CF69018C627fB44cCC76eC89f); // HLN + tokenOut = IERC20(0xe7cd86e13AC4309349F30B3435a9d337750fC82D); // USDT0 + poolInOut = 0xA7C9E7343bD8f1eb7000F25dE5aeb52c6B78B1b7; // ENOSYS_V3_POOL + } +} diff --git a/test/solidity/Periphery/LDA/Facets/HyperswapV3Facet.t.sol b/test/solidity/Periphery/LDA/Facets/HyperswapV3Facet.t.sol new file mode 100644 index 000000000..a5d1847ac --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/HyperswapV3Facet.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IHyperswapV3Factory } from "lifi/Interfaces/IHyperswapV3Factory.sol"; +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { BaseUniV3StyleDEXFacetTest } from "../BaseUniV3StyleDEXFacet.t.sol"; + +/// @title HyperswapV3FacetTest +/// @notice Fork-based UniV3-style tests for HyperswapV3 integration. +/// @dev Selects Hyperevm fork, sets pool/token addresses, and delegates logic to base UniV3 test helpers. +contract HyperswapV3FacetTest is BaseUniV3StyleDEXFacetTest { + // ==== Setup Functions ==== + + /// @notice Selects `hyperevm` network and block for fork tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "hyperevm", + blockNumber: 4433562 + }); + } + + /// @notice Returns the Hyperswap-specific callback selector expected to be invoked by pools. + function _getCallbackSelector() internal pure override returns (bytes4) { + return UniV3StyleFacet.hyperswapV3SwapCallback.selector; + } + + /// @notice Resolves tokenIn/out and pool address for Hyperswap V3 USDT0/WHYPE pair. + function _setupDexEnv() internal override { + tokenIn = IERC20(0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb); // USDT0 + tokenOut = IERC20(0x5555555555555555555555555555555555555555); // WHYPE + poolInOut = IHyperswapV3Factory( + 0xB1c0fa0B789320044A6F623cFe5eBda9562602E3 + ).getPool(address(tokenIn), address(tokenOut), 3000); + } + + /// @notice Default input amount adapted to 6 decimals for USDT0 on Hyperevm. + function _getDefaultAmountForTokenIn() + internal + pure + override + returns (uint256) + { + return 1_000 * 1e6; + } +} diff --git a/test/solidity/Periphery/LDA/Facets/IzumiV3Facet.t.sol b/test/solidity/Periphery/LDA/Facets/IzumiV3Facet.t.sol new file mode 100644 index 000000000..2384d5639 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/IzumiV3Facet.t.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IzumiV3Facet } from "lifi/Periphery/LDA/Facets/IzumiV3Facet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseDEXFacetWithCallbackTest } from "../BaseDEXFacetWithCallback.t.sol"; +import { MockNoCallbackPool } from "../../../utils/MockNoCallbackPool.sol"; + +/// @title IzumiV3FacetTest +/// @notice Forked + local tests for Izumi V3 pools routed through LDA. +/// @dev Validates swap paths, aggregator/user flows, multi-hop, callback auth, and revert cases. +contract IzumiV3FacetTest is BaseDEXFacetWithCallbackTest { + /// @notice Facet proxy handle bound to diamond after facet cut. + IzumiV3Facet internal izumiV3Facet; + + // ==== Types ==== + + /// @notice Parameters for a single Izumi V3 swap step. + /// @param pool Target pool. + /// @param direction Direction of the swap. + /// @param destinationAddress Address receiving the proceeds. + struct IzumiV3SwapParams { + address pool; + SwapDirection direction; + address destinationAddress; + } + + // ==== Errors ==== + + /// @notice Emitted when callback amounts are non-positive (guard against bad pool behavior). + error IzumiV3SwapCallbackNotPositiveAmount(); + + // ==== Setup Functions ==== + + /// @notice Selects Base fork and block height used by the tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "base", + blockNumber: 29831758 + }); + } + + /// @notice Deploys facet and returns swap + callback selectors for diamond cut. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + izumiV3Facet = new IzumiV3Facet(); + bytes4[] memory functionSelectors = new bytes4[](3); + functionSelectors[0] = izumiV3Facet.swapIzumiV3.selector; + functionSelectors[1] = izumiV3Facet.swapX2YCallback.selector; + functionSelectors[2] = izumiV3Facet.swapY2XCallback.selector; + return (address(izumiV3Facet), functionSelectors); + } + + /// @notice Sets `izumiV3Facet` to the diamond proxy. + function _setFacetInstance(address payable ldaDiamond) internal override { + izumiV3Facet = IzumiV3Facet(ldaDiamond); + } + + /// @notice Defines a USDC/WETH/USDB_C path and pools on Base for tests. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913); // USDC + tokenMid = IERC20(0x4200000000000000000000000000000000000006); // WETH + tokenOut = IERC20(0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA); // USDB_C + poolInMid = 0xb92A9A91a9F7E8e6Bb848508A6DaF08f9D718554; // WETH/USDC + poolMidOut = 0xdb5D62f06EEcEf0Da7506e0700c2f03c57016De5; // WETH/USDB_C + } + + /// @notice Default amount for USDC (6 decimals) used in tests. + function _getDefaultAmountForTokenIn() + internal + pure + override + returns (uint256) + { + return 100 * 1e6; // 100 USDC with 6 decimals + } + + // ==== Callback Test Hooks ==== + + /// @notice Chooses which callback to use under the base callback tests. + function _getCallbackSelector() internal view override returns (bytes4) { + return izumiV3Facet.swapX2YCallback.selector; + } + + /// @notice Supplies a no-callback pool for negative tests. + function _deployNoCallbackPool() internal override returns (address) { + return address(new MockNoCallbackPool()); + } + + /// @notice Encodes swap payload for callback arming tests. + /// @param pool Pool to use. + /// @param destinationAddress Destination address of proceeds. + function _buildCallbackSwapData( + address pool, + address destinationAddress + ) internal pure override returns (bytes memory) { + return + abi.encodePacked( + IzumiV3Facet.swapIzumiV3.selector, + pool, + uint8(1), // direction TOKEN0_TO_TOKEN1 + destinationAddress + ); + } + + // ==== Test Cases ==== + + /// @notice User-funded swap USDC->WETH on poolInMid, sending to USER_RECEIVER. + function test_CanSwap() public override { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildIzumiV3SwapData( + IzumiV3SwapParams({ + pool: poolInMid, + direction: SwapDirection.Token1ToToken0, + destinationAddress: USER_RECEIVER + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Aggregator-funded swap USDC->WETH on poolInMid to USER_SENDER. + function test_CanSwap_FromDexAggregator() public override { + // Test USDC -> WETH + deal( + address(tokenIn), + address(coreRouteFacet), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildIzumiV3SwapData( + IzumiV3SwapParams({ + pool: poolInMid, + direction: SwapDirection.Token1ToToken0, + destinationAddress: USER_SENDER + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn() - 1, // -1 for undrain protection + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Multi-hop user->aggregator USDC->WETH->USDB_C flow. + function test_CanSwap_MultiHop() public override { + // Fund the sender with tokens + uint256 amountIn = _getDefaultAmountForTokenIn(); + deal(address(tokenIn), USER_SENDER, amountIn); + + // Build first swap data: USDC -> WETH + bytes memory firstSwapData = _buildIzumiV3SwapData( + IzumiV3SwapParams({ + pool: poolInMid, + direction: SwapDirection.Token1ToToken0, + destinationAddress: address(coreRouteFacet) + }) + ); + + // Build second swap data: WETH -> USDB_C + bytes memory secondSwapData = _buildIzumiV3SwapData( + IzumiV3SwapParams({ + pool: poolMidOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_SENDER + }) + ); + + // Prepare params for both hops + SwapTestParams[] memory params = new SwapTestParams[](2); + bytes[] memory swapData = new bytes[](2); + + // First hop: USDC -> WETH + params[0] = SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: address(coreRouteFacet), + commandType: CommandType.DistributeUserERC20 + }); + swapData[0] = firstSwapData; + + // Second hop: WETH -> USDB_C + params[1] = SwapTestParams({ + tokenIn: address(tokenMid), + tokenOut: address(tokenOut), + amountIn: 0, // Will be determined by first swap + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }); + swapData[1] = secondSwapData; + + bytes memory route = _buildMultiHopRoute(params, swapData); + + vm.startPrank(USER_SENDER); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route + ); + + vm.stopPrank(); + } + + /// @notice Negative test: callback should revert when amounts are not positive. + function testRevert_IzumiV3SwapCallbackNotPositiveAmount() public { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.store( + address(ldaDiamond), + keccak256("com.lifi.lda.callbackAuthenticator"), + bytes32(uint256(uint160(poolInMid))) + ); + + vm.prank(poolInMid); + vm.expectRevert(IzumiV3SwapCallbackNotPositiveAmount.selector); + izumiV3Facet.swapY2XCallback(0, 0, abi.encode(tokenIn)); + } + + /// @notice Negative test: too-large amount encodings must revert with InvalidCallData. + function testRevert_FailsIfAmountInIsTooLarge() public { + deal(address(tokenMid), USER_SENDER, type(uint256).max); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildIzumiV3SwapData( + IzumiV3SwapParams({ + pool: poolInMid, + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_RECEIVER + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenMid), + tokenOut: address(tokenIn), + amountIn: type(uint216).max, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + // ==== Helper Functions ==== + + /// @notice Encodes Izumi V3 swap payloads for route steps. + /// @param params Pool/direction/destinationAddress. + /// @return Packed calldata for `swapIzumiV3`. + function _buildIzumiV3SwapData( + IzumiV3SwapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + izumiV3Facet.swapIzumiV3.selector, + params.pool, + uint8(params.direction), + params.destinationAddress + ); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/KatanaV3Facet.t.sol b/test/solidity/Periphery/LDA/Facets/KatanaV3Facet.t.sol new file mode 100644 index 000000000..a284bc528 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/KatanaV3Facet.t.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { KatanaV3Facet } from "lifi/Periphery/LDA/Facets/KatanaV3Facet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseDEXFacetTest } from "../BaseDEXFacet.t.sol"; + +/// @title KatanaV3FacetTest +/// @notice Ronin Katana V3 +contract KatanaV3FacetTest is BaseDEXFacetTest { + /// @notice Facet proxy bound to the diamond after setup. + KatanaV3Facet internal katanaV3Facet; + + // ==== Types ==== + + /// @notice Swap data payload packed for KatanaV3Facet. + struct KatanaV3SwapData { + address pool; + SwapDirection direction; + address destinationAddress; + } + + // ==== Setup Functions ==== + + /// @notice Picks Optimism fork and block height. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "ronin", + blockNumber: 47105304 + }); + } + + /// @notice Deploys facet and returns its swap selector. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + katanaV3Facet = new KatanaV3Facet(); + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = KatanaV3Facet.swapKatanaV3.selector; + return (address(katanaV3Facet), functionSelectors); + } + + /// @notice Sets the facet instance to the diamond proxy. + function _setFacetInstance(address payable ldaDiamond) internal override { + katanaV3Facet = KatanaV3Facet(ldaDiamond); + } + + /// @notice Assigns tokens used in tests; pool addresses are resolved per-test from the router. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x0B7007c13325C48911F73A2daD5FA5dCBf808aDc); // USDC + tokenOut = IERC20(0xe514d9DEB7966c8BE0ca922de8a064264eA6bcd4); // WRAPPED_RON + poolInOut = 0x392d372F2A51610E9AC5b741379D5631Ca9A1c7f; // USDC_WRAPPED_RON_POOL + } + + /// @notice Default amount for 6-decimal tokens on Optimism. + function _getDefaultAmountForTokenIn() + internal + pure + override + returns (uint256) + { + return 1_000 * 1e6; + } + + // ==== Test Cases ==== + + function test_CanSwap() public override { + // Transfer 1 000 crvUSD from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildKatanaV3SwapData( + KatanaV3SwapData({ + pool: poolInOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: address(USER_SENDER) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + function test_CanSwap_FromDexAggregator() public override { + // Fund the aggregator with 1000 USDC + deal( + address(tokenIn), + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildKatanaV3SwapData( + KatanaV3SwapData({ + pool: poolInOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: address(USER_SENDER) + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn() - 1, // Account for slot-undrain + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + function test_CanSwap_MultiHop() public override { + // SKIPPED: KatanaV3 multi-hop unsupported due to AS requirement. + // KatanaV3 (being a similar implementation to UniV3) does not support a "one-pool" second hop today, + // because the aggregator (ProcessOnePool) always passes amountSpecified = 0 into + // the pool.swap call. UniV3-style pools immediately revert on + // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools in a single processRoute invocation. + } + + function testRevert_KatanaV3InvalidPool() public { + vm.startPrank(USER_SENDER); + + // build route with invalid pool address + bytes memory route = _buildKatanaV3SwapData( + KatanaV3SwapData({ + pool: address(0), + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_SENDER + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + function testRevert_KatanaV3InvalidRecipient() public { + vm.startPrank(USER_SENDER); + + // build route with invalid recipient + bytes memory route = _buildKatanaV3SwapData( + KatanaV3SwapData({ + pool: poolInOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: address(0) // invalid recipient + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Empty test as KatanaV3 does not use callbacks for regular swaps + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + /// @dev Note: While KatanaV3 has flashloan callbacks, they are separate from swap callbacks + function testRevert_CallbackFromUnexpectedSender() public override { + // KatanaV3 does not use callbacks for swaps - test intentionally empty + } + + /// @notice Empty test as KatanaV3 does not use callbacks for regular swaps + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + /// @dev Note: While KatanaV3 has flashloan callbacks, they are separate from swap callbacks + function testRevert_SwapWithoutCallback() public override { + // KatanaV3 does not use callbacks for swaps - test intentionally empty + } + + // ==== Helper Functions ==== + + /// @notice Encodes swap payload for KatanaV3Facet.swapKatanaV3. + /// @param params pool/direction/destinationAddress/callback status. + /// @return Packed bytes payload. + function _buildKatanaV3SwapData( + KatanaV3SwapData memory params + ) private pure returns (bytes memory) { + return + abi.encodePacked( + KatanaV3Facet.swapKatanaV3.selector, + params.pool, + uint8(params.direction), + params.destinationAddress + ); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/LaminarV3Facet.t.sol b/test/solidity/Periphery/LDA/Facets/LaminarV3Facet.t.sol new file mode 100644 index 000000000..08c577352 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/LaminarV3Facet.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { BaseUniV3StyleDEXFacetTest } from "../BaseUniV3StyleDEXFacet.t.sol"; + +/// @title LaminarV3FacetTest +/// @notice Hyperevm UniV3-style tests for Laminar pools via LDA. +/// @dev Minimal setup; inherits all execution helpers from the base. +contract LaminarV3FacetTest is BaseUniV3StyleDEXFacetTest { + /// @notice Selects Hyperevm fork and block used by tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "hyperevm", + blockNumber: 4433562 + }); + } + + /// @notice Returns Laminar V3 callback selector used during swaps. + function _getCallbackSelector() internal pure override returns (bytes4) { + return UniV3StyleFacet.laminarV3SwapCallback.selector; + } + + /// @notice Sets tokenIn/out and pool for Laminar V3 on Hyperevm. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x5555555555555555555555555555555555555555); // WHYPE + tokenOut = IERC20(0x5748ae796AE46A4F1348a1693de4b50560485562); // LHYPE + poolInOut = 0xdAA8a66380fb35b35CB7bc1dBC1925AbfdD0ae45; // WHYPE_LHYPE_POOL + } +} diff --git a/test/solidity/Periphery/LDA/Facets/NativeWrapperFacet.t.sol b/test/solidity/Periphery/LDA/Facets/NativeWrapperFacet.t.sol new file mode 100644 index 000000000..1153b7d23 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/NativeWrapperFacet.t.sol @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IWETH } from "lifi/Interfaces/IWETH.sol"; +import { BaseCoreRouteTest } from "../BaseCoreRoute.t.sol"; +import { NativeWrapperFacet } from "lifi/Periphery/LDA/Facets/NativeWrapperFacet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; + +/// @title NativeWrapperFacetTest +/// @notice Tests for NativeWrapperFacet wrapping/unwrapping functionality +/// @dev Verifies unwrapNative (WETH->ETH) and wrapNative (ETH->WETH) operations with various funding sources +contract NativeWrapperFacetTest is BaseCoreRouteTest { + /// @notice Facet proxy for native wrapping/unwrapping operations + NativeWrapperFacet internal nativeWrapperFacet; + + /// @notice WETH token instance for testing + IWETH internal weth; + + // ==== Custom Errors ==== + + /// @dev Thrown when native token operations fail + error NativeTransferFailed(); + /// @dev Thrown when WETH operations fail + error WETHOperationFailed(); + + // ==== Types ==== + + /// @notice Parameters for unwrapNative operation + /// @param destinationAddress Address to receive unwrapped ETH + struct UnwrapParams { + address destinationAddress; + } + + /// @notice Parameters for wrapNative operation + /// @param wrappedNative WETH contract address + /// @param destinationAddress Address to receive wrapped tokens + struct WrapParams { + address wrappedNative; + address destinationAddress; + } + + // ==== Setup Functions ==== + + function setUp() public override { + // Set fork config for mainnet WETH testing + customBlockNumberForForking = 23228012; + customRpcUrlForForking = "ETH_NODE_URI_MAINNET"; + + fork(); + super.setUp(); + + // Deploy and register NativeWrapperFacet + nativeWrapperFacet = new NativeWrapperFacet(); + bytes4[] memory functionSelectors = new bytes4[](2); + functionSelectors[0] = nativeWrapperFacet.unwrapNative.selector; + functionSelectors[1] = nativeWrapperFacet.wrapNative.selector; + + addFacet( + address(ldaDiamond), + address(nativeWrapperFacet), + functionSelectors + ); + + // Set facet instance to diamond proxy + nativeWrapperFacet = NativeWrapperFacet(address(ldaDiamond)); + + // Setup WETH token + weth = IWETH(ADDRESS_WRAPPED_NATIVE); // Use constant from TestBase + } + + // ==== Test Cases ==== + + /// @notice Tests unwrapping WETH to ETH from user funds + function test_CanUnwrap() public { + uint256 amountIn = 1 ether; + + // Fund user with WETH + deal(address(weth), USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + // Record initial ETH balance + uint256 initialETHBalance = USER_RECEIVER.balance; + + bytes memory swapData = _buildUnwrapSwapData( + UnwrapParams({ destinationAddress: USER_RECEIVER }) + ); + + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(weth), + tokenOut: address(0), // Native ETH + amountIn: amountIn, + minOut: amountIn, // Expect 1:1 unwrapping + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeUserERC20 + }); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route); + + // Verify ETH was received + assertEq(USER_RECEIVER.balance, initialETHBalance + amountIn); + + vm.stopPrank(); + } + + /// @notice Tests unwrapping WETH to ETH from aggregator funds + function test_CanUnwrap_FromDexAggregator() public { + uint256 amountIn = 1 ether; + + // Fund aggregator with WETH + deal(address(weth), address(ldaDiamond), amountIn + 1); + + vm.startPrank(USER_SENDER); + + // Record initial ETH balance + uint256 initialETHBalance = USER_RECEIVER.balance; + + bytes memory swapData = _buildUnwrapSwapData( + UnwrapParams({ destinationAddress: USER_RECEIVER }) + ); + + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(weth), + tokenOut: address(0), // Native ETH + amountIn: amountIn, + minOut: amountIn, // Expect 1:1 unwrapping + sender: address(ldaDiamond), + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeSelfERC20 + }); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route); + + // Verify ETH was received + assertEq(USER_RECEIVER.balance, initialETHBalance + amountIn); + + vm.stopPrank(); + } + + /// @notice Tests wrapping ETH to WETH + function test_CanWrap2() public { + uint256 amountIn = 1 ether; + + // Fund aggregator with ETH + vm.deal(USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + // // Record initial WETH balance + uint256 initialWETHBalance = weth.balanceOf(USER_RECEIVER); + + bytes memory swapData = _buildWrapSwapData( + WrapParams({ + wrappedNative: address(weth), + destinationAddress: USER_RECEIVER + }) + ); + + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(0), // Native ETH + tokenOut: address(weth), + amountIn: amountIn, + minOut: amountIn, // Expect 1:1 wrapping + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeNative + }); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route); + + // Verify WETH was received + assertEq(weth.balanceOf(USER_RECEIVER), initialWETHBalance + amountIn); + + vm.stopPrank(); + } + + /// @notice Tests wrapping ETH to WETH and keeping on aggregator + function test_CanWrap_ToAggregator() public { + uint256 amountIn = 1 ether; + + // Fund aggregator with ETH + vm.deal(USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + // Record initial WETH balance + uint256 initialWETHBalance = weth.balanceOf(address(ldaDiamond)); + + bytes memory swapData = _buildWrapSwapData( + WrapParams({ + wrappedNative: address(weth), + destinationAddress: address(ldaDiamond) + }) + ); + + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(0), // Native ETH + tokenOut: address(weth), + amountIn: amountIn, + minOut: amountIn, // Expect 1:1 wrapping + sender: USER_SENDER, + destinationAddress: address(ldaDiamond), + commandType: CommandType.DistributeNative + }); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route); + + // Verify WETH was received by aggregator + assertEq( + weth.balanceOf(address(ldaDiamond)), + initialWETHBalance + amountIn + ); + + vm.stopPrank(); + } + + /// @notice Tests that wrapNative reverts with zero wrapped native address + function testRevert_WrapNative_ZeroWrappedNative() public { + uint256 amountIn = 1 ether; + + // Fund user with ETH (not aggregator, since this is DistributeNative) + vm.deal(USER_SENDER, amountIn); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildWrapSwapData( + WrapParams({ + wrappedNative: address(0), // Invalid wrapped native + destinationAddress: USER_RECEIVER + }) + ); + + SwapTestParams memory params = SwapTestParams({ + tokenIn: address(0), + tokenOut: address(weth), + amountIn: amountIn, + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_RECEIVER, + commandType: CommandType.DistributeNative + }); + + bytes memory route = _buildBaseRoute(params, swapData); + _executeAndVerifySwap(params, route, InvalidCallData.selector); + + vm.stopPrank(); + } + + // ==== Helper Functions ==== + + /// @notice Builds swap data for unwrapNative operation + /// @param params Unwrap parameters + /// @return Packed swap data with selector and parameters + function _buildUnwrapSwapData( + UnwrapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + nativeWrapperFacet.unwrapNative.selector, + params.destinationAddress + ); + } + + /// @notice Builds swap data for wrapNative operation + /// @param params Wrap parameters + /// @return Packed swap data with selector and parameters + function _buildWrapSwapData( + WrapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + nativeWrapperFacet.wrapNative.selector, + params.wrappedNative, + params.destinationAddress + ); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/PancakeV2.t.sol b/test/solidity/Periphery/LDA/Facets/PancakeV2.t.sol new file mode 100644 index 000000000..1048b3f48 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/PancakeV2.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IUniV2StylePool } from "lifi/Interfaces/IUniV2StylePool.sol"; +import { BaseUniV2StyleDEXFacetTest } from "../BaseUniV2StyleDEXFacet.t.sol"; +import { WrongPoolReserves } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; + +/// @title PancakeV2FacetTest +/// @notice Fork-based UniV2-style tests for PancakeV2 integration. +/// @dev Selects BSC fork, sets pool/token addresses, and delegates logic to base UniV2 test helpers. +contract PancakeV2FacetTest is BaseUniV2StyleDEXFacetTest { + // ==== Setup Functions ==== + + /// @notice Selects `bsc` network and block for fork tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ networkName: "bsc", blockNumber: 58868609 }); + } + + /// @notice Resolves tokenIn/out and pool address for Pancake V2 BUSD/WBNB pair. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x55d398326f99059fF775485246999027B3197955); // BUSD + tokenOut = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c); // WBNB + poolInOut = 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE; // Pancake V2 BUSD/WBNB pool + } + + /// @notice Returns the pool fee for Pancake V2 BUSD/WBNB pair. + /// @return fee The pool fee in basis points (e.g., 2500 for 0.25%) + function _getPoolFee() internal pure override returns (uint24) { + return 2500; + } + + /// @notice Tests that the facet reverts when pool reserves are zero + function testRevert_WrongPoolReserves() public { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + IERC20(address(tokenIn)).approve( + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + // Mock getReserves to return zero reserves + vm.mockCall( + poolInOut, + abi.encodeWithSelector(IUniV2StylePool.getReserves.selector), + abi.encode(0, 0, block.timestamp) + ); + + bytes memory swapData = _buildUniV2SwapData( + UniV2SwapParams({ + pool: poolInOut, + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_SENDER, + fee: _getPoolFee() + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + WrongPoolReserves.selector + ); + + // Clean up mock + vm.clearMockedCalls(); + vm.stopPrank(); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/RabbitSwapV3.t.sol b/test/solidity/Periphery/LDA/Facets/RabbitSwapV3.t.sol new file mode 100644 index 000000000..d1c9caa2b --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/RabbitSwapV3.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { BaseUniV3StyleDEXFacetTest } from "../BaseUniV3StyleDEXFacet.t.sol"; + +/// @title RabbitSwapV3FacetTest +/// @notice Viction UniV3-style tests for RabbitSwap V3 pools. +/// @dev Covers invalid pool/destinationAddress edge cases plus standard setup. +contract RabbitSwapV3FacetTest is BaseUniV3StyleDEXFacetTest { + /// @notice Selects Viction fork and block height used in tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "viction", + blockNumber: 94490946 + }); + } + + /// @notice Returns RabbitSwap V3 callback selector used during swaps. + function _getCallbackSelector() internal pure override returns (bytes4) { + return UniV3StyleFacet.rabbitSwapV3SwapCallback.selector; + } + + /// @notice Sets tokenIn/out and pool address for RabbitSwap V3 on Viction. + function _setupDexEnv() internal override { + tokenIn = IERC20(0xB786D9c8120D311b948cF1e5Aa48D8fBacf477E2); // SOROS + tokenOut = IERC20(0x0Fd0288AAAE91eaF935e2eC14b23486f86516c8C); // C98 + poolInOut = 0xF10eFaE2DdAC396c4ef3c52009dB429A120d0C0D; // pool + } + + /// @notice Negative: zero pool must be rejected by facet call data validation. + function testRevert_RabbitSwapInvalidPool() public { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + bytes memory swapData = _buildUniV3SwapData( + UniV3SwapParams({ + pool: address(0), // Invalid pool address + direction: SwapDirection.Token1ToToken0, + destinationAddress: USER_SENDER + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Negative: zero destinationAddress must be rejected by facet call data validation. + function testRevert_RabbitSwapInvalidDestinationAddress() public { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + bytes memory swapData = _buildUniV3SwapData( + UniV3SwapParams({ + pool: poolInOut, + direction: SwapDirection.Token1ToToken0, + destinationAddress: address(0) // Invalid destination address + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/SyncSwapV2Facet.t.sol b/test/solidity/Periphery/LDA/Facets/SyncSwapV2Facet.t.sol new file mode 100644 index 000000000..102b96035 --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/SyncSwapV2Facet.t.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { BaseDEXFacetTest } from "../BaseDEXFacet.t.sol"; +import { SyncSwapV2Facet } from "lifi/Periphery/LDA/Facets/SyncSwapV2Facet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; + +/// @title SyncSwapV2FacetTest +/// @notice Linea SyncSwap V2 tests via LDA route; includes both v1 and v2 pool wiring. +/// @dev Verifies single-hop, aggregator flow, multi-hop (with DispatchSinglePoolSwap), and revert paths. +contract SyncSwapV2FacetTest is BaseDEXFacetTest { + /// @notice Facet proxy for swaps bound to the diamond after setup. + SyncSwapV2Facet internal syncSwapV2Facet; + + /// @notice SyncSwap vault address used by V1 pools. + address internal constant SYNC_SWAP_VAULT = + address(0x7160570BB153Edd0Ea1775EC2b2Ac9b65F1aB61B); + + /// @notice A Linea v2 pool used by specific tests. + address internal constant USDC_WETH_POOL_V2 = + address(0xDDed227D71A096c6B5D87807C1B5C456771aAA94); + + /// @notice Selects Linea fork and block height used by tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "linea", + blockNumber: 20077881 + }); + } + + /// @notice Deploys SyncSwapV2Facet and returns its swap selector for diamond cut. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + syncSwapV2Facet = new SyncSwapV2Facet(); + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = syncSwapV2Facet.swapSyncSwapV2.selector; + return (address(syncSwapV2Facet), functionSelectors); + } + + /// @notice Sets the facet instance to the diamond proxy after facet cut. + function _setFacetInstance(address payable ldaDiamond) internal override { + syncSwapV2Facet = SyncSwapV2Facet(ldaDiamond); + } + + /// @notice Defines tokens and pools used by tests (WETH/USDC/USDT). + function _setupDexEnv() internal override { + tokenIn = IERC20(0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f); // WETH + tokenMid = IERC20(0x176211869cA2b568f2A7D4EE941E073a821EE1ff); // USDC + tokenOut = IERC20(0xA219439258ca9da29E9Cc4cE5596924745e12B93); // USDT + poolInMid = 0x5Ec5b1E9b1Bd5198343ABB6E55Fb695d2F7Bb308; // WETH-USDC V1 + poolMidOut = 0x258d5f860B11ec73Ee200eB14f1b60A3B7A536a2; // USDC-USDT V1 + } + + /// @notice Single‐pool swap: USER sends WETH -> receives USDC. + function test_CanSwap() public override { + // Transfer 1 000 WETH from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolInMid, + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice User-funded swap on a V2 pool variant. + function test_CanSwap_PoolV2() public { + // Transfer 1 000 WETH from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: USDC_WETH_POOL_V2, + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 0, + vault: SYNC_SWAP_VAULT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Aggregator-funded swap on a V1 pool; uses amountIn-1 for undrain protection. + function test_CanSwap_FromDexAggregator() public override { + // Fund the aggregator with 1 000 WETH + deal( + address(tokenIn), + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolInMid, + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn() - 1, // Account for slot-undrain + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Aggregator-funded swap on a V2 pool; same undrain behavior. + function test_CanSwap_FromDexAggregator_PoolV2() public { + // Fund the aggregator with 1 000 WETH + deal( + address(tokenIn), + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: USDC_WETH_POOL_V2, + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 0, + vault: SYNC_SWAP_VAULT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn() - 1, // Account for slot-undrain + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }), + swapData + ); + + vm.stopPrank(); + } + + /// @notice Multi-hop WETH->USDC (v1) then USDC->USDT (v1) where hop2 consumes hop1 outputs. + function test_CanSwap_MultiHop() public override { + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + // Build swap data for both hops + bytes memory firstSwapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolInMid, + to: SYNC_SWAP_VAULT, + withdrawMode: 2, + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + bytes memory secondSwapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolMidOut, + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + // Prepare params for both hops + SwapTestParams[] memory params = new SwapTestParams[](2); + bytes[] memory swapData = new bytes[](2); + + // First hop: WETH -> USDC + params[0] = SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: SYNC_SWAP_VAULT, + commandType: CommandType.DistributeUserERC20 + }); + swapData[0] = firstSwapData; + + // Second hop: USDC -> USDT + params[1] = SwapTestParams({ + tokenIn: address(tokenMid), + tokenOut: address(tokenOut), + amountIn: 0, // Not used in DispatchSinglePoolSwap + minOut: 0, + sender: address(ldaDiamond), + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeSelfERC20 + }); + swapData[1] = secondSwapData; + + bytes memory route = _buildMultiHopRoute(params, swapData); + + // Use _executeAndVerifySwap with first and last token of the chain + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route + ); + + vm.stopPrank(); + } + + /// @notice V1 pools require non-zero vault; zero must revert. + function testRevert_V1PoolMissingVaultAddress() public { + // Transfer 1 000 WETH from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapData = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolInOut, + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 1, + vault: address(0) // Invalid vault address + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Invalid pool/destinationAddress combinations should revert. + function testRevert_InvalidPoolOrDestinationAddress() public { + // Transfer 1 000 WETH from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes memory swapDataWithInvalidPool = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: address(0), + to: address(USER_SENDER), + withdrawMode: 2, + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, // Send to next pool + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataWithInvalidPool, + InvalidCallData.selector + ); + + bytes + memory swapDataWithInvalidDestinationAddress = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolInOut, + to: address(0), + withdrawMode: 2, + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataWithInvalidDestinationAddress, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Only withdrawMode 0/1/2 are supported; invalid modes must revert. + function testRevert_InvalidWithdrawMode() public { + // Transfer 1 000 WETH from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + bytes + memory swapDataWithInvalidWithdrawMode = _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams({ + pool: poolInMid, + to: address(USER_SENDER), + withdrawMode: 3, // Invalid withdraw mode (>2) + isV1Pool: 1, + vault: SYNC_SWAP_VAULT + }) + ); + + // Approve tokens for the swap + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataWithInvalidWithdrawMode, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice V1 pools with zero target address should revert + function testRevert_V1PoolZeroTarget() public { + // Transfer 1 000 WETH from whale to USER_SENDER + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + vm.startPrank(USER_SENDER); + + // Swap data with isV1Pool=true but zero target (vault) address + bytes memory swapData = abi.encodePacked( + syncSwapV2Facet.swapSyncSwapV2.selector, + poolInMid, // pool address + USER_SENDER, // destination address + uint8(2), // withdrawMode + uint8(1), // isV1Pool = true + address(0) // target/vault address = zero address + ); + + // Approve tokens for the swap + tokenIn.approve(address(ldaDiamond), _getDefaultAmountForTokenIn()); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapData, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + /// @notice Empty test as SyncSwapV2 does not use callbacks + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + function testRevert_CallbackFromUnexpectedSender() public override { + // SyncSwapV2 does not use callbacks - test intentionally empty + } + + /// @notice Empty test as SyncSwapV2 does not use callbacks + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + function testRevert_SwapWithoutCallback() public override { + // SyncSwapV2 does not use callbacks - test intentionally empty + } + + /// @notice SyncSwap V2 swap parameter shape used for `swapSyncSwapV2`. + struct SyncSwapV2SwapParams { + address pool; + address to; + uint8 withdrawMode; + uint8 isV1Pool; + address vault; + } + + /// @notice Builds SyncSwapV2 swap payload for route steps. + /// @param params pool/to/withdrawMode/isV1Pool/vault tuple. + function _buildSyncSwapV2SwapData( + SyncSwapV2SwapParams memory params + ) internal view returns (bytes memory) { + return + abi.encodePacked( + syncSwapV2Facet.swapSyncSwapV2.selector, + params.pool, + params.to, + params.withdrawMode, + params.isV1Pool, + params.vault + ); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/VelodromeV2Facet.t.sol b/test/solidity/Periphery/LDA/Facets/VelodromeV2Facet.t.sol new file mode 100644 index 000000000..9d497136a --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/VelodromeV2Facet.t.sol @@ -0,0 +1,917 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IVelodromeV2Pool } from "lifi/Interfaces/IVelodromeV2Pool.sol"; +import { IVelodromeV2PoolCallee } from "lifi/Interfaces/IVelodromeV2PoolCallee.sol"; +import { IVelodromeV2PoolFactory } from "lifi/Interfaces/IVelodromeV2PoolFactory.sol"; +import { IVelodromeV2Router } from "lifi/Interfaces/IVelodromeV2Router.sol"; +import { VelodromeV2Facet } from "lifi/Periphery/LDA/Facets/VelodromeV2Facet.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { WrongPoolReserves } from "lifi/Periphery/LDA/LiFiDEXAggregatorErrors.sol"; +import { BaseDEXFacetTest } from "../BaseDEXFacet.t.sol"; + +/// @title VelodromeV2FacetTest +/// @notice Optimism Velodrome V2 tests covering stable/volatile pools, aggregator/user flows, multi-hop, and precise reserve accounting. +/// @dev Includes a flashloan callback path to assert event expectations and reserve deltas. +contract VelodromeV2FacetTest is BaseDEXFacetTest { + /// @notice Facet proxy bound to the diamond after setup. + VelodromeV2Facet internal velodromeV2Facet; + + // ==== Constants ==== + /// @notice Router used to compute amounts and resolve pools. + IVelodromeV2Router internal constant VELODROME_V2_ROUTER = + IVelodromeV2Router(0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858); // optimism router + /// @notice Factory registry used by the router for pool lookup. + address internal constant VELODROME_V2_FACTORY_REGISTRY = + 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a; + + /// @notice Mock receiver for exercising the pool's flashloan callback hook. + MockVelodromeV2FlashLoanCallbackReceiver + internal mockFlashloanCallbackReceiver; + + // ==== Types ==== + /// @notice Enables/disables the flashloan callback during swap. + enum CallbackStatus { + Disabled, // 0 + Enabled // 1 + } + + /// @notice Encapsulates a single Velodrome V2 swap request under test. + struct VelodromeV2SwapTestParams { + address from; + address to; + address tokenIn; + uint256 amountIn; + address tokenOut; + bool stable; + SwapDirection direction; + CallbackStatus callbackStatus; + } + + /// @notice Parameters and precomputed amounts used by multi-hop tests. + struct MultiHopTestParams { + address tokenIn; + address tokenMid; + address tokenOut; + address pool1; + address pool2; + uint256[] amounts1; + uint256[] amounts2; + uint256 pool1Fee; + uint256 pool2Fee; + } + + /// @notice Snapshot of reserve states across two pools for before/after assertions. + struct ReserveState { + uint256 reserve0Pool1; + uint256 reserve1Pool1; + uint256 reserve0Pool2; + uint256 reserve1Pool2; + } + + /// @notice Swap data payload packed for VelodromeV2Facet. + struct VelodromeV2SwapData { + address pool; + SwapDirection direction; + address destinationAddress; + CallbackStatus callbackStatus; + } + + // ==== Setup Functions ==== + + /// @notice Picks Optimism fork and block height. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ + networkName: "optimism", + blockNumber: 133999121 + }); + } + + /// @notice Deploys facet and returns its swap selector. + function _createFacetAndSelectors() + internal + override + returns (address, bytes4[] memory) + { + velodromeV2Facet = new VelodromeV2Facet(); + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = velodromeV2Facet.swapVelodromeV2.selector; + return (address(velodromeV2Facet), functionSelectors); + } + + /// @notice Sets the facet instance to the diamond proxy. + function _setFacetInstance(address payable ldaDiamond) internal override { + velodromeV2Facet = VelodromeV2Facet(ldaDiamond); + } + + /// @notice Assigns tokens used in tests; pool addresses are resolved per-test from the router. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85); // USDC + tokenMid = IERC20(0x296F55F8Fb28E498B858d0BcDA06D955B2Cb3f97); // STG + tokenOut = IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607); // USDC.e + // pools vary by test; and they are fetched inside tests + } + + /// @notice Default amount for 6-decimal tokens on Optimism. + function _getDefaultAmountForTokenIn() + internal + pure + override + returns (uint256) + { + return 1_000 * 1e6; + } + + // ==== Test Cases ==== + + function test_CanSwap() public override { + deal( + address(tokenIn), + address(USER_SENDER), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + _testSwap( + VelodromeV2SwapTestParams({ + from: address(USER_SENDER), + to: address(USER_SENDER), + tokenIn: address(tokenIn), + amountIn: _getDefaultAmountForTokenIn(), + tokenOut: address(tokenOut), + stable: false, + direction: SwapDirection.Token0ToToken1, + callbackStatus: CallbackStatus.Disabled + }) + ); + + vm.stopPrank(); + } + + function test_CanSwap_NoStable_Reverse() public { + // first perform the forward swap. + test_CanSwap(); + + uint256 amountIn = IERC20(address(tokenOut)).balanceOf(USER_SENDER); + vm.startPrank(USER_SENDER); + + _testSwap( + VelodromeV2SwapTestParams({ + from: USER_SENDER, + to: USER_SENDER, + tokenIn: address(tokenOut), // USDC.e from first swap + amountIn: amountIn, + tokenOut: address(tokenIn), // USDC + stable: false, + direction: SwapDirection.Token1ToToken0, + callbackStatus: CallbackStatus.Disabled + }) + ); + vm.stopPrank(); + } + + function test_CanSwap_Stable() public { + deal( + address(tokenIn), + address(USER_SENDER), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + _testSwap( + VelodromeV2SwapTestParams({ + from: USER_SENDER, + to: USER_SENDER, + tokenIn: address(tokenIn), + amountIn: _getDefaultAmountForTokenIn(), + tokenOut: address(tokenOut), + stable: true, + direction: SwapDirection.Token0ToToken1, + callbackStatus: CallbackStatus.Disabled + }) + ); + vm.stopPrank(); + } + + function test_CanSwap_Stable_Reverse() public { + // first perform the forward stable swap. + test_CanSwap_Stable(); + + vm.startPrank(USER_SENDER); + + _testSwap( + VelodromeV2SwapTestParams({ + from: USER_SENDER, + to: USER_SENDER, + tokenIn: address(tokenOut), + amountIn: _getDefaultAmountForTokenIn() / 2, + tokenOut: address(tokenIn), + stable: true, + direction: SwapDirection.Token1ToToken0, + callbackStatus: CallbackStatus.Disabled + }) + ); + vm.stopPrank(); + } + + function test_CanSwap_FromDexAggregator() public override { + // fund dex aggregator contract so that the contract holds USDC + deal( + address(tokenIn), + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + _testSwap( + VelodromeV2SwapTestParams({ + from: address(ldaDiamond), + to: address(USER_SENDER), + tokenIn: address(tokenIn), + amountIn: _getDefaultAmountForTokenIn() - 1, // adjust for slot undrain protection: subtract 1 token so that the + // aggregator's balance isn't completely drained, matching the contract's safeguard + tokenOut: address(tokenOut), + stable: false, + direction: SwapDirection.Token0ToToken1, + callbackStatus: CallbackStatus.Disabled + }) + ); + vm.stopPrank(); + } + + function test_CanSwap_FlashloanCallback() public { + deal( + address(tokenIn), + address(USER_SENDER), + _getDefaultAmountForTokenIn() + ); + + mockFlashloanCallbackReceiver = new MockVelodromeV2FlashLoanCallbackReceiver(); + + vm.startPrank(USER_SENDER); + _testSwap( + VelodromeV2SwapTestParams({ + from: address(USER_SENDER), + to: address(mockFlashloanCallbackReceiver), + tokenIn: address(tokenIn), + amountIn: _getDefaultAmountForTokenIn(), + tokenOut: address(tokenOut), + stable: false, + direction: SwapDirection.Token0ToToken1, + callbackStatus: CallbackStatus.Enabled + }) + ); + vm.stopPrank(); + } + + function test_CanSwap_MultiHop() public override { + deal( + address(tokenIn), + address(USER_SENDER), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + // Setup routes and get amounts + MultiHopTestParams memory params = _setupRoutes( + address(tokenIn), + address(tokenMid), + address(tokenOut), + false, + false + ); + + // Get initial reserves BEFORE the swap + ReserveState memory initialReserves; + ( + initialReserves.reserve0Pool1, + initialReserves.reserve1Pool1, + + ) = IVelodromeV2Pool(params.pool1).getReserves(); + ( + initialReserves.reserve0Pool2, + initialReserves.reserve1Pool2, + + ) = IVelodromeV2Pool(params.pool2).getReserves(); + + // Build route and execute swap + SwapTestParams[] memory swapParams = new SwapTestParams[](2); + bytes[] memory swapData = new bytes[](2); + + // First hop: USDC -> USDC.e (stable) + swapParams[0] = SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenMid, + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: params.pool2, // Send to next pool + commandType: CommandType.DistributeUserERC20 + }); + + // Build first hop swap data + swapData[0] = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: params.pool1, + direction: SwapDirection.Token0ToToken1, + destinationAddress: params.pool2, + callbackStatus: CallbackStatus.Disabled + }) + ); + + // Second hop: USDC.e -> STG (volatile) + swapParams[1] = SwapTestParams({ + tokenIn: params.tokenMid, + tokenOut: params.tokenOut, + amountIn: params.amounts1[1], // Use output from first hop + sender: params.pool2, + minOut: 0, + destinationAddress: USER_SENDER, // Send to next pool + commandType: CommandType.DispatchSinglePoolSwap + }); + + // Build second hop swap data + swapData[1] = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: params.pool2, + direction: SwapDirection.Token0ToToken1, + destinationAddress: USER_SENDER, + callbackStatus: CallbackStatus.Disabled + }) + ); + + // Use the base _buildMultiHopRoute + bytes memory route = _buildMultiHopRoute(swapParams, swapData); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route + ); + _verifyReserves(params, initialReserves); + + vm.stopPrank(); + } + + function test_CanSwap_MultiHop_WithStable() public { + deal( + address(tokenIn), + address(USER_SENDER), + _getDefaultAmountForTokenIn() + ); + + vm.startPrank(USER_SENDER); + + // Setup routes and get amounts for stable->volatile path + MultiHopTestParams memory params = _setupRoutes( + address(tokenIn), + address(tokenOut), + address(tokenMid), + true, // stable pool for first hop + false // volatile pool for second hop + ); + + // Get initial reserves BEFORE the swap + ReserveState memory initialReserves; + ( + initialReserves.reserve0Pool1, + initialReserves.reserve1Pool1, + + ) = IVelodromeV2Pool(params.pool1).getReserves(); + ( + initialReserves.reserve0Pool2, + initialReserves.reserve1Pool2, + + ) = IVelodromeV2Pool(params.pool2).getReserves(); + + // Build route and execute swap + SwapTestParams[] memory hopParams = new SwapTestParams[](2); + bytes[] memory hopData = new bytes[](2); + + // First hop: USDC -> USDC.e (stable) + hopParams[0] = SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenMid, + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: params.pool2, // Send to next pool + commandType: CommandType.DistributeUserERC20 + }); + + hopData[0] = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: params.pool1, + direction: SwapDirection.Token0ToToken1, + destinationAddress: params.pool2, + callbackStatus: CallbackStatus.Disabled + }) + ); + + // Second hop: USDC.e -> STG (volatile) + hopParams[1] = SwapTestParams({ + tokenIn: params.tokenMid, + tokenOut: params.tokenOut, + amountIn: params.amounts1[1], // Use output from first hop + sender: params.pool2, + minOut: 0, + destinationAddress: USER_SENDER, + commandType: CommandType.DispatchSinglePoolSwap + }); + + hopData[1] = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: params.pool2, + direction: SwapDirection.Token1ToToken0, + destinationAddress: USER_SENDER, + callbackStatus: CallbackStatus.Disabled + }) + ); + + bytes memory route = _buildMultiHopRoute(hopParams, hopData); + + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + route + ); + _verifyReserves(params, initialReserves); + vm.stopPrank(); + } + + function testRevert_InvalidPoolOrDestinationAddress() public { + vm.startPrank(USER_SENDER); + + // Get a valid pool address first for comparison + address validPool = VELODROME_V2_ROUTER.poolFor( + address(tokenIn), + address(tokenMid), + false, + VELODROME_V2_FACTORY_REGISTRY + ); + + // --- Test case 1: Zero pool address --- + // 1. Create the specific swap data blob + bytes memory swapDataZeroPool = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: address(0), // Invalid pool + direction: SwapDirection.Token1ToToken0, + destinationAddress: USER_SENDER, + callbackStatus: CallbackStatus.Disabled + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataZeroPool, + InvalidCallData.selector + ); + + // --- Test case 2: Zero destination address --- + bytes + memory swapDataZeroDestinationAddress = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: validPool, + direction: SwapDirection.Token1ToToken0, + destinationAddress: address(0), // Invalid destination address + callbackStatus: CallbackStatus.Disabled + }) + ); + + _buildRouteAndExecuteAndVerifySwap( + SwapTestParams({ + tokenIn: address(tokenIn), + tokenOut: address(tokenMid), + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: USER_SENDER, + commandType: CommandType.DistributeUserERC20 + }), + swapDataZeroDestinationAddress, + InvalidCallData.selector + ); + + vm.stopPrank(); + } + + function testRevert_WrongPoolReserves() public { + vm.startPrank(USER_SENDER); + + // Setup multi-hop route: USDC -> STG -> USDC.e + MultiHopTestParams memory params = _setupRoutes( + address(tokenIn), + address(tokenMid), + address(tokenOut), + false, + false + ); + + // Build multi-hop route + SwapTestParams[] memory hopParams = new SwapTestParams[](2); + bytes[] memory hopData = new bytes[](2); + + // First hop: USDC -> USDC.e (stable) + hopParams[0] = SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenMid, + amountIn: _getDefaultAmountForTokenIn(), + minOut: 0, + sender: USER_SENDER, + destinationAddress: params.pool2, // Send to next pool + commandType: CommandType.DistributeUserERC20 + }); + + hopData[0] = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: params.pool1, + direction: SwapDirection.Token0ToToken1, + destinationAddress: params.pool2, + callbackStatus: CallbackStatus.Disabled + }) + ); + + // Second hop: USDC.e -> STG (volatile) + hopParams[1] = SwapTestParams({ + tokenIn: params.tokenMid, + tokenOut: params.tokenOut, + amountIn: 0, // Not used in DispatchSinglePoolSwap + minOut: 0, + sender: params.pool2, + destinationAddress: USER_SENDER, + commandType: CommandType.DispatchSinglePoolSwap + }); + + hopData[1] = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: params.pool2, + direction: SwapDirection.Token1ToToken0, + destinationAddress: USER_SENDER, + callbackStatus: CallbackStatus.Disabled + }) + ); + + bytes memory route = _buildMultiHopRoute(hopParams, hopData); + + deal(address(tokenIn), USER_SENDER, _getDefaultAmountForTokenIn()); + + IERC20(address(tokenIn)).approve( + address(ldaDiamond), + _getDefaultAmountForTokenIn() + ); + + // Mock getReserves for the second pool (which uses dispatchSinglePoolSwap) to return zero reserves + vm.mockCall( + params.pool2, + abi.encodeWithSelector(IVelodromeV2Pool.getReserves.selector), + abi.encode(0, 0, block.timestamp) + ); + + vm.expectRevert(WrongPoolReserves.selector); + + coreRouteFacet.processRoute( + address(tokenIn), + _getDefaultAmountForTokenIn(), + address(tokenOut), + 0, + USER_SENDER, + route + ); + + vm.stopPrank(); + vm.clearMockedCalls(); + } + + /// @notice Empty test as VelodromeV2 does not use callbacks for regular swaps + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + /// @dev Note: While VelodromeV2 has flashloan callbacks, they are separate from swap callbacks + function testRevert_CallbackFromUnexpectedSender() public override { + // VelodromeV2 does not use callbacks for swaps - test intentionally empty + } + + /// @notice Empty test as VelodromeV2 does not use callbacks for regular swaps + /// @dev Explicitly left empty as this DEX's architecture doesn't require callback verification + /// @dev Note: While VelodromeV2 has flashloan callbacks, they are separate from swap callbacks + function testRevert_SwapWithoutCallback() public override { + // VelodromeV2 does not use callbacks for swaps - test intentionally empty + } + + // ==== Helper Functions ==== + + /// @notice Helper to execute a VelodromeV2 swap with optional callback expectation and strict event checking. + /// @param params The swap request including direction and whether callback is enabled. + /// @dev Computes expected outputs via router, builds payload, and asserts Route + optional HookCalled event. + function _testSwap(VelodromeV2SwapTestParams memory params) internal { + // get expected output amounts from the router. + IVelodromeV2Router.Route[] + memory routes = new IVelodromeV2Router.Route[](1); + routes[0] = IVelodromeV2Router.Route({ + from: params.tokenIn, + to: params.tokenOut, + stable: params.stable, + factory: address(VELODROME_V2_FACTORY_REGISTRY) + }); + uint256[] memory expectedOutput = VELODROME_V2_ROUTER.getAmountsOut( + params.amountIn, + routes + ); + + // Retrieve the pool address. + address pool = VELODROME_V2_ROUTER.poolFor( + params.tokenIn, + params.tokenOut, + params.stable, + VELODROME_V2_FACTORY_REGISTRY + ); + + // if tokens come from the aggregator (address(ldaDiamond)), use command code 1; otherwise, use 2. + CommandType commandCode = params.from == address(ldaDiamond) + ? CommandType.DistributeSelfERC20 + : CommandType.DistributeUserERC20; + + // 1. Pack the data for the specific swap FIRST + bytes memory swapData = _buildVelodromeV2SwapData( + VelodromeV2SwapData({ + pool: pool, + direction: params.direction, + destinationAddress: params.to, + callbackStatus: params.callbackStatus + }) + ); + // build the route. + bytes memory route = _buildBaseRoute( + SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: params.amountIn, + minOut: expectedOutput[1], + sender: params.from, + destinationAddress: params.to, + commandType: commandCode + }), + swapData + ); + + ExpectedEvent[] memory expectedEvents = new ExpectedEvent[](1); + if (params.callbackStatus == CallbackStatus.Enabled) { + bytes[] memory eventParams = new bytes[](4); + eventParams[0] = abi.encode(address(ldaDiamond)); + eventParams[1] = abi.encode(uint256(0)); + eventParams[2] = abi.encode(uint256(0)); + eventParams[3] = abi.encode(abi.encode(params.tokenIn)); + + expectedEvents[0] = ExpectedEvent({ + checkTopic1: false, + checkTopic2: false, + checkTopic3: false, + checkData: false, + eventSelector: keccak256( + "HookCalled(address,uint256,uint256,bytes)" + ), + eventParams: eventParams, + indexedParamIndices: new uint8[](0) + }); + } else { + expectedEvents = new ExpectedEvent[](0); + } + + // execute the swap + _executeAndVerifySwap( + SwapTestParams({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountIn: params.amountIn, + minOut: expectedOutput[1], + sender: params.from, + destinationAddress: params.to, + commandType: params.from == address(ldaDiamond) + ? CommandType.DistributeSelfERC20 + : CommandType.DistributeUserERC20 + }), + route, + expectedEvents, + false, // Not a fee-on-transfer token + RouteEventVerification({ + expectedExactOut: expectedOutput[1], + checkData: true + }) + ); + } + + /// @notice Builds routes and computes amounts for two-hop paths using the router. + /// @param tokenIn First hop input. + /// @param tokenMid Intermediate token between hops. + /// @param tokenOut Final output token. + /// @param isStableFirst Whether hop1 uses a stable pool. + /// @param isStableSecond Whether hop2 uses a stable pool. + /// @return params MultiHopTestParams including pool addresses, amounts and pool fees. + function _setupRoutes( + address tokenIn, + address tokenMid, + address tokenOut, + bool isStableFirst, + bool isStableSecond + ) private view returns (MultiHopTestParams memory params) { + params.tokenIn = tokenIn; + params.tokenMid = tokenMid; + params.tokenOut = tokenOut; + + // Setup first hop route + IVelodromeV2Router.Route[] + memory routes1 = new IVelodromeV2Router.Route[](1); + routes1[0] = IVelodromeV2Router.Route({ + from: tokenIn, + to: tokenMid, + stable: isStableFirst, + factory: address(VELODROME_V2_FACTORY_REGISTRY) + }); + params.amounts1 = VELODROME_V2_ROUTER.getAmountsOut( + _getDefaultAmountForTokenIn(), + routes1 + ); + + // Setup second hop route + IVelodromeV2Router.Route[] + memory routes2 = new IVelodromeV2Router.Route[](1); + routes2[0] = IVelodromeV2Router.Route({ + from: tokenMid, + to: tokenOut, + stable: isStableSecond, + factory: address(VELODROME_V2_FACTORY_REGISTRY) + }); + params.amounts2 = VELODROME_V2_ROUTER.getAmountsOut( + params.amounts1[1], + routes2 + ); + + // Get pool addresses + params.pool1 = VELODROME_V2_ROUTER.poolFor( + tokenIn, + tokenMid, + isStableFirst, + VELODROME_V2_FACTORY_REGISTRY + ); + + params.pool2 = VELODROME_V2_ROUTER.poolFor( + tokenMid, + tokenOut, + isStableSecond, + VELODROME_V2_FACTORY_REGISTRY + ); + + // Get pool fees info + params.pool1Fee = IVelodromeV2PoolFactory( + VELODROME_V2_FACTORY_REGISTRY + ).getFee(params.pool1, isStableFirst); + params.pool2Fee = IVelodromeV2PoolFactory( + VELODROME_V2_FACTORY_REGISTRY + ).getFee(params.pool2, isStableSecond); + + return params; + } + + /// @notice Encodes swap payload for VelodromeV2Facet.swapVelodromeV2. + /// @param params pool/direction/destinationAddress/callback status. + /// @return Packed bytes payload. + function _buildVelodromeV2SwapData( + VelodromeV2SwapData memory params + ) private pure returns (bytes memory) { + return + abi.encodePacked( + VelodromeV2Facet.swapVelodromeV2.selector, + params.pool, + uint8(params.direction), + params.destinationAddress, + params.callbackStatus + ); + } + + /// @notice Verifies exact reserve deltas on both pools against computed amounts and fees. + /// @param params Multi-hop parameters returned by `_setupRoutes`. + /// @param initialReserves Reserves captured before the swap. + function _verifyReserves( + MultiHopTestParams memory params, + ReserveState memory initialReserves + ) private { + // Get reserves after swap + ( + uint256 finalReserve0Pool1, + uint256 finalReserve1Pool1, + + ) = IVelodromeV2Pool(params.pool1).getReserves(); + ( + uint256 finalReserve0Pool2, + uint256 finalReserve1Pool2, + + ) = IVelodromeV2Pool(params.pool2).getReserves(); + + address token0Pool1 = IVelodromeV2Pool(params.pool1).token0(); + address token0Pool2 = IVelodromeV2Pool(params.pool2).token0(); + + // Calculate exact expected changes + uint256 amountInAfterFees = _getDefaultAmountForTokenIn() - + ((_getDefaultAmountForTokenIn() * params.pool1Fee) / 10000); + + // Assert exact reserve changes for Pool1 + if (token0Pool1 == params.tokenIn) { + // tokenIn is token0, so reserve0 should increase and reserve1 should decrease + assertEq( + finalReserve0Pool1 - initialReserves.reserve0Pool1, + amountInAfterFees, + "Pool1 reserve0 (tokenIn) change incorrect" + ); + assertEq( + initialReserves.reserve1Pool1 - finalReserve1Pool1, + params.amounts1[1], + "Pool1 reserve1 (tokenMid) change incorrect" + ); + } else { + // tokenIn is token1, so reserve1 should increase and reserve0 should decrease + assertEq( + finalReserve1Pool1 - initialReserves.reserve1Pool1, + amountInAfterFees, + "Pool1 reserve1 (tokenIn) change incorrect" + ); + assertEq( + initialReserves.reserve0Pool1 - finalReserve0Pool1, + params.amounts1[1], + "Pool1 reserve0 (tokenMid) change incorrect" + ); + } + + // Assert exact reserve changes for Pool2 + if (token0Pool2 == params.tokenMid) { + // tokenMid is token0, so reserve0 should increase and reserve1 should decrease + assertEq( + finalReserve0Pool2 - initialReserves.reserve0Pool2, + params.amounts1[1] - + ((params.amounts1[1] * params.pool2Fee) / 10000), + "Pool2 reserve0 (tokenMid) change incorrect" + ); + assertEq( + initialReserves.reserve1Pool2 - finalReserve1Pool2, + params.amounts2[1], + "Pool2 reserve1 (tokenOut) change incorrect" + ); + } else { + // tokenMid is token1, so reserve1 should increase and reserve0 should decrease + assertEq( + finalReserve1Pool2 - initialReserves.reserve1Pool2, + params.amounts1[1] - + ((params.amounts1[1] * params.pool2Fee) / 10000), + "Pool2 reserve1 (tokenMid) change incorrect" + ); + assertEq( + initialReserves.reserve0Pool2 - finalReserve0Pool2, + params.amounts2[1], + "Pool2 reserve0 (tokenOut) change incorrect" + ); + } + } +} + +contract MockVelodromeV2FlashLoanCallbackReceiver is IVelodromeV2PoolCallee { + // ==== Events ==== + /// @notice Emitted by the mock to validate callback plumbing during tests. + event HookCalled( + address sender, + uint256 amount0, + uint256 amount1, + bytes data + ); + + /// @notice Simple hook that emits `HookCalled` with passthrough data. + function hook( + address sender, + uint256 amount0, + uint256 amount1, + bytes calldata data + ) external { + emit HookCalled(sender, amount0, amount1, data); + } +} diff --git a/test/solidity/Periphery/LDA/Facets/XSwapV3Facet.t.sol b/test/solidity/Periphery/LDA/Facets/XSwapV3Facet.t.sol new file mode 100644 index 000000000..c48a749bc --- /dev/null +++ b/test/solidity/Periphery/LDA/Facets/XSwapV3Facet.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UniV3StyleFacet } from "lifi/Periphery/LDA/Facets/UniV3StyleFacet.sol"; +import { BaseUniV3StyleDEXFacetTest } from "../BaseUniV3StyleDEXFacet.t.sol"; + +/// @title XSwapV3FacetTest +/// @notice XDC chain UniV3-style tests for XSwap V3 integration via LDA. +/// @dev Minimal setup; inherits execution logic from base UniV3-style test harness. +contract XSwapV3FacetTest is BaseUniV3StyleDEXFacetTest { + // ==== Setup Functions ==== + + /// @notice Selects XDC fork and block height used by the tests. + function _setupForkConfig() internal override { + forkConfig = ForkConfig({ networkName: "xdc", blockNumber: 89279495 }); + } + + /// @notice Returns the XSwap V3 callback selector used during swaps. + function _getCallbackSelector() internal pure override returns (bytes4) { + return UniV3StyleFacet.xswapCallback.selector; + } + + /// @notice Sets tokenIn/out and the pool for XSwap V3 on XDC. + function _setupDexEnv() internal override { + tokenIn = IERC20(0x2A8E898b6242355c290E1f4Fc966b8788729A4D4); // USDC.e + tokenOut = IERC20(0x951857744785E80e2De051c32EE7b25f9c458C42); // WXDC + poolInOut = 0x81B4afF811E94fb084A0d3B3ca456D09AeC14EB0; // pool + } + + /// @notice Default input amount for USDC.e (6 decimals). + function _getDefaultAmountForTokenIn() + internal + pure + override + returns (uint256) + { + return 1_000 * 1e6; + } +} diff --git a/test/solidity/Periphery/LDA/LiFiDEXAggregatorDiamond.t.sol b/test/solidity/Periphery/LDA/LiFiDEXAggregatorDiamond.t.sol new file mode 100644 index 000000000..588f060e6 --- /dev/null +++ b/test/solidity/Periphery/LDA/LiFiDEXAggregatorDiamond.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LiFiDEXAggregatorDiamond } from "lifi/Periphery/LDA/LiFiDEXAggregatorDiamond.sol"; +import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; +import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; +import { BaseDiamondTest } from "../../utils/BaseDiamondTest.sol"; + +/// @notice Spins up a minimal LDA (LiFi DEX Aggregator) Diamond with loupe, ownership, and emergency pause facets for periphery tests. +contract LiFiDEXAggregatorDiamondTest is BaseDiamondTest { + LiFiDEXAggregatorDiamond public ldaDiamond; + + function setUp() public virtual override { + super.setUp(); // This creates the main LiFiDiamond as 'diamond' + + ldaDiamond = new LiFiDEXAggregatorDiamond( + USER_DIAMOND_OWNER, + address(diamondCutFacet) + ); + + // prepare function selector for diamondCut (OwnershipFacet) + bytes4[] memory functionSelectors = new bytes4[](1); + functionSelectors[0] = ownershipFacet.owner.selector; + + // prepare parameters for diamondCut (OwnershipFacet) + LibDiamond.FacetCut[] memory cut = new LibDiamond.FacetCut[](1); + cut[0] = LibDiamond.FacetCut({ + facetAddress: address(ownershipFacet), + action: LibDiamond.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + vm.prank(USER_DIAMOND_OWNER); + DiamondCutFacet(address(ldaDiamond)).diamondCut(cut, address(0), ""); + } + + function test_DeploysWithoutErrors() public virtual override { + ldaDiamond = new LiFiDEXAggregatorDiamond( + USER_DIAMOND_OWNER, + address(diamondCutFacet) + ); + super.test_DeploysWithoutErrors(); + } +} diff --git a/test/solidity/Periphery/LiFiDEXAggregator.t.sol b/test/solidity/Periphery/LiFiDEXAggregator.t.sol deleted file mode 100644 index 37cb5da8c..000000000 --- a/test/solidity/Periphery/LiFiDEXAggregator.t.sol +++ /dev/null @@ -1,3563 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.17; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IVelodromeV2Pool } from "lifi/Interfaces/IVelodromeV2Pool.sol"; -import { IVelodromeV2PoolCallee } from "lifi/Interfaces/IVelodromeV2PoolCallee.sol"; -import { IVelodromeV2PoolFactory } from "lifi/Interfaces/IVelodromeV2PoolFactory.sol"; -import { IVelodromeV2Router } from "lifi/Interfaces/IVelodromeV2Router.sol"; -import { IAlgebraPool } from "lifi/Interfaces/IAlgebraPool.sol"; -import { IAlgebraRouter } from "lifi/Interfaces/IAlgebraRouter.sol"; -import { IAlgebraFactory } from "lifi/Interfaces/IAlgebraFactory.sol"; -import { IAlgebraQuoter } from "lifi/Interfaces/IAlgebraQuoter.sol"; -import { IHyperswapV3Factory } from "lifi/Interfaces/IHyperswapV3Factory.sol"; -import { IHyperswapV3QuoterV2 } from "lifi/Interfaces/IHyperswapV3QuoterV2.sol"; -import { LiFiDEXAggregator } from "lifi/Periphery/LiFiDEXAggregator.sol"; -import { InvalidConfig, InvalidCallData } from "lifi/Errors/GenericErrors.sol"; -import { TestBase } from "../utils/TestBase.sol"; -import { TestToken as ERC20 } from "../utils/TestToken.sol"; -import { MockFeeOnTransferToken } from "../utils/MockTokenFeeOnTransfer.sol"; - -// Command codes for route processing -enum CommandType { - None, // 0 - not used - ProcessMyERC20, // 1 - processMyERC20 - ProcessUserERC20, // 2 - processUserERC20 - ProcessNative, // 3 - processNative - ProcessOnePool, // 4 - processOnePool - ProcessInsideBento, // 5 - processInsideBento - ApplyPermit // 6 - applyPermit -} - -// Pool type identifiers -enum PoolType { - UniV2, // 0 - UniV3, // 1 - WrapNative, // 2 - BentoBridge, // 3 - Trident, // 4 - Curve, // 5 - VelodromeV2, // 6 - Algebra, // 7 - iZiSwap, // 8 - SyncSwapV2, // 9 - KatanaV3 // 10 -} - -// Direction constants -enum SwapDirection { - Token1ToToken0, // 0 - Token0ToToken1 // 1 -} - -// Callback constants -enum CallbackStatus { - Disabled, // 0 - Enabled // 1 -} - -// Other constants -uint16 constant FULL_SHARE = 65535; // 100% share for single pool swaps - -contract MockVelodromeV2FlashLoanCallbackReceiver is IVelodromeV2PoolCallee { - event HookCalled( - address sender, - uint256 amount0, - uint256 amount1, - bytes data - ); - - function hook( - address sender, - uint256 amount0, - uint256 amount1, - bytes calldata data - ) external { - emit HookCalled(sender, amount0, amount1, data); - } -} -/** - * @title LiFiDexAggregatorTest - * @notice Base test contract with common functionality and abstractions for DEX-specific tests - */ -abstract contract LiFiDexAggregatorTest is TestBase { - using SafeERC20 for IERC20; - - // Common variables - LiFiDEXAggregator internal liFiDEXAggregator; - address[] internal privileged; - - // Common events and errors - event Route( - address indexed from, - address to, - address indexed tokenIn, - address indexed tokenOut, - uint256 amountIn, - uint256 amountOutMin, - uint256 amountOut - ); - event HookCalled( - address sender, - uint256 amount0, - uint256 amount1, - bytes data - ); - - error WrongPoolReserves(); - error PoolDoesNotExist(); - - // helper function to initialize the aggregator - function _initializeDexAggregator(address owner) internal { - privileged = new address[](1); - privileged[0] = owner; - - liFiDEXAggregator = new LiFiDEXAggregator( - address(0xCAFE), - privileged, - owner - ); - vm.label(address(liFiDEXAggregator), "LiFiDEXAggregator"); - } - - // Setup function for Apechain tests - function setupApechain() internal { - customRpcUrlForForking = "ETH_NODE_URI_APECHAIN"; - customBlockNumberForForking = 12912470; - fork(); - - _initializeDexAggregator(address(USER_DIAMOND_OWNER)); - } - - function setupHyperEVM() internal { - customRpcUrlForForking = "ETH_NODE_URI_HYPEREVM"; - customBlockNumberForForking = 4433562; - fork(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - function setUp() public virtual { - initTestBase(); - vm.label(USER_SENDER, "USER_SENDER"); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - function test_ContractIsSetUpCorrectly() public { - assertEq(address(liFiDEXAggregator.BENTO_BOX()), address(0xCAFE)); - assertEq( - liFiDEXAggregator.priviledgedUsers(address(USER_DIAMOND_OWNER)), - true - ); - assertEq(liFiDEXAggregator.owner(), USER_DIAMOND_OWNER); - } - - function testRevert_FailsIfOwnerIsZeroAddress() public { - vm.expectRevert(InvalidConfig.selector); - - liFiDEXAggregator = new LiFiDEXAggregator( - address(0xCAFE), - privileged, - address(0) - ); - } - - // ============================ Abstract DEX Tests ============================ - /** - * @notice Abstract test for basic token swapping functionality - * Each DEX implementation should override this - */ - function test_CanSwap() public virtual { - // Each DEX implementation must override this - // solhint-disable-next-line gas-custom-errors - revert("test_CanSwap: Not implemented"); - } - - /** - * @notice Abstract test for swapping tokens from the DEX aggregator - * Each DEX implementation should override this - */ - function test_CanSwap_FromDexAggregator() public virtual { - // Each DEX implementation must override this - // solhint-disable-next-line gas-custom-errors - revert("test_CanSwap_FromDexAggregator: Not implemented"); - } - - /** - * @notice Abstract test for multi-hop swapping - * Each DEX implementation should override this - */ - function test_CanSwap_MultiHop() public virtual { - // Each DEX implementation must override this - // solhint-disable-next-line gas-custom-errors - revert("test_CanSwap_MultiHop: Not implemented"); - } -} - -/** - * @title VelodromeV2 tests - * @notice Tests specific to Velodrome V2 pool type - */ -contract LiFiDexAggregatorVelodromeV2Test is LiFiDexAggregatorTest { - // ==================== Velodrome V2 specific variables ==================== - IVelodromeV2Router internal constant VELODROME_V2_ROUTER = - IVelodromeV2Router(0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858); // optimism router - address internal constant VELODROME_V2_FACTORY_REGISTRY = - 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a; - IERC20 internal constant STG_TOKEN = - IERC20(0x296F55F8Fb28E498B858d0BcDA06D955B2Cb3f97); - IERC20 internal constant USDC_E_TOKEN = - IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607); - - MockVelodromeV2FlashLoanCallbackReceiver - internal mockFlashloanCallbackReceiver; - - // Velodrome V2 structs - struct VelodromeV2SwapTestParams { - address from; - address to; - address tokenIn; - uint256 amountIn; - address tokenOut; - bool stable; - SwapDirection direction; - bool callback; - } - - struct MultiHopTestParams { - address tokenIn; - address tokenMid; - address tokenOut; - address pool1; - address pool2; - uint256[] amounts1; - uint256[] amounts2; - uint256 pool1Fee; - uint256 pool2Fee; - } - - struct ReserveState { - uint256 reserve0Pool1; - uint256 reserve1Pool1; - uint256 reserve0Pool2; - uint256 reserve1Pool2; - } - - // Setup function for Optimism tests - function setupOptimism() internal { - customRpcUrlForForking = "ETH_NODE_URI_OPTIMISM"; - customBlockNumberForForking = 133999121; - initTestBase(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - function setUp() public override { - setupOptimism(); - } - - // // ============================ Velodrome V2 Tests ============================ - - // no stable swap - function test_CanSwap() public override { - vm.startPrank(USER_SENDER); - - _testSwap( - VelodromeV2SwapTestParams({ - from: address(USER_SENDER), - to: address(USER_SENDER), - tokenIn: ADDRESS_USDC, - amountIn: 1_000 * 1e6, - tokenOut: address(STG_TOKEN), - stable: false, - direction: SwapDirection.Token0ToToken1, - callback: false - }) - ); - - vm.stopPrank(); - } - - function test_CanSwap_NoStable_Reverse() public { - // first perform the forward swap. - test_CanSwap(); - - vm.startPrank(USER_SENDER); - _testSwap( - VelodromeV2SwapTestParams({ - from: USER_SENDER, - to: USER_SENDER, - tokenIn: address(STG_TOKEN), - amountIn: 500 * 1e18, - tokenOut: ADDRESS_USDC, - stable: false, - direction: SwapDirection.Token1ToToken0, - callback: false - }) - ); - vm.stopPrank(); - } - - function test_CanSwap_Stable() public { - vm.startPrank(USER_SENDER); - _testSwap( - VelodromeV2SwapTestParams({ - from: USER_SENDER, - to: USER_SENDER, - tokenIn: ADDRESS_USDC, - amountIn: 1_000 * 1e6, - tokenOut: address(USDC_E_TOKEN), - stable: true, - direction: SwapDirection.Token0ToToken1, - callback: false - }) - ); - vm.stopPrank(); - } - - function test_CanSwap_Stable_Reverse() public { - // first perform the forward stable swap. - test_CanSwap_Stable(); - - vm.startPrank(USER_SENDER); - - _testSwap( - VelodromeV2SwapTestParams({ - from: USER_SENDER, - to: USER_SENDER, - tokenIn: address(USDC_E_TOKEN), - amountIn: 500 * 1e6, - tokenOut: ADDRESS_USDC, - stable: false, - direction: SwapDirection.Token1ToToken0, - callback: false - }) - ); - vm.stopPrank(); - } - - function test_CanSwap_FromDexAggregator() public override { - // fund dex aggregator contract so that the contract holds USDC - deal(ADDRESS_USDC, address(liFiDEXAggregator), 100_000 * 1e6); - - vm.startPrank(USER_SENDER); - _testSwap( - VelodromeV2SwapTestParams({ - from: address(liFiDEXAggregator), - to: address(USER_SENDER), - tokenIn: ADDRESS_USDC, - amountIn: IERC20(ADDRESS_USDC).balanceOf( - address(liFiDEXAggregator) - ) - 1, // adjust for slot undrain protection: subtract 1 token so that the aggregator's balance - // isn't completely drained, matching the contract's safeguard - tokenOut: address(USDC_E_TOKEN), - stable: false, - direction: SwapDirection.Token0ToToken1, - callback: false - }) - ); - vm.stopPrank(); - } - - function test_CanSwap_FlashloanCallback() public { - mockFlashloanCallbackReceiver = new MockVelodromeV2FlashLoanCallbackReceiver(); - - vm.startPrank(USER_SENDER); - _testSwap( - VelodromeV2SwapTestParams({ - from: address(USER_SENDER), - to: address(mockFlashloanCallbackReceiver), - tokenIn: ADDRESS_USDC, - amountIn: 1_000 * 1e6, - tokenOut: address(USDC_E_TOKEN), - stable: false, - direction: SwapDirection.Token0ToToken1, - callback: true - }) - ); - vm.stopPrank(); - } - - // Override the abstract test with VelodromeV2 implementation - function test_CanSwap_MultiHop() public override { - vm.startPrank(USER_SENDER); - - // Setup routes and get amounts - MultiHopTestParams memory params = _setupRoutes( - ADDRESS_USDC, - address(STG_TOKEN), - address(USDC_E_TOKEN), - false, - false - ); - - // Get initial reserves BEFORE the swap - ReserveState memory initialReserves; - ( - initialReserves.reserve0Pool1, - initialReserves.reserve1Pool1, - - ) = IVelodromeV2Pool(params.pool1).getReserves(); - ( - initialReserves.reserve0Pool2, - initialReserves.reserve1Pool2, - - ) = IVelodromeV2Pool(params.pool2).getReserves(); - - uint256 initialBalance1 = IERC20(params.tokenIn).balanceOf( - USER_SENDER - ); - uint256 initialBalance2 = IERC20(params.tokenOut).balanceOf( - USER_SENDER - ); - - // Build route and execute swap - bytes memory route = _buildMultiHopRoute(params, USER_SENDER, 1, 1); - - // Approve and execute - IERC20(params.tokenIn).approve(address(liFiDEXAggregator), 1000 * 1e6); - - vm.expectEmit(true, true, true, true); - emit Route( - USER_SENDER, - USER_SENDER, - params.tokenIn, - params.tokenOut, - 1000 * 1e6, - params.amounts2[1], - params.amounts2[1] - ); - - liFiDEXAggregator.processRoute( - params.tokenIn, - 1000 * 1e6, - params.tokenOut, - params.amounts2[1], - USER_SENDER, - route - ); - - _verifyUserBalances(params, initialBalance1, initialBalance2); - _verifyReserves(params, initialReserves); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop_WithStable() public { - vm.startPrank(USER_SENDER); - - // Setup routes and get amounts for stable->volatile path - MultiHopTestParams memory params = _setupRoutes( - ADDRESS_USDC, - address(USDC_E_TOKEN), - address(STG_TOKEN), - true, // stable pool for first hop - false // volatile pool for second hop - ); - - // Get initial reserves BEFORE the swap - ReserveState memory initialReserves; - ( - initialReserves.reserve0Pool1, - initialReserves.reserve1Pool1, - - ) = IVelodromeV2Pool(params.pool1).getReserves(); - ( - initialReserves.reserve0Pool2, - initialReserves.reserve1Pool2, - - ) = IVelodromeV2Pool(params.pool2).getReserves(); - - // Record initial balances - uint256 initialBalance1 = IERC20(params.tokenIn).balanceOf( - USER_SENDER - ); - uint256 initialBalance2 = IERC20(params.tokenOut).balanceOf( - USER_SENDER - ); - - // Build route and execute swap - bytes memory route = _buildMultiHopRoute(params, USER_SENDER, 1, 0); - - // Approve and execute - IERC20(params.tokenIn).approve(address(liFiDEXAggregator), 1000 * 1e6); - - vm.expectEmit(true, true, true, true); - emit Route( - USER_SENDER, - USER_SENDER, - params.tokenIn, - params.tokenOut, - 1000 * 1e6, - params.amounts2[1], - params.amounts2[1] - ); - - liFiDEXAggregator.processRoute( - params.tokenIn, - 1000 * 1e6, - params.tokenOut, - params.amounts2[1], - USER_SENDER, - route - ); - - _verifyUserBalances(params, initialBalance1, initialBalance2); - _verifyReserves(params, initialReserves); - - vm.stopPrank(); - } - - function testRevert_InvalidPoolOrRecipient() public { - vm.startPrank(USER_SENDER); - - // Get a valid pool address first for comparison - address validPool = VELODROME_V2_ROUTER.poolFor( - ADDRESS_USDC, - address(STG_TOKEN), - false, - VELODROME_V2_FACTORY_REGISTRY - ); - - // Test case 1: Zero pool address - bytes memory routeWithZeroPool = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - ADDRESS_USDC, - uint8(1), - FULL_SHARE, - uint8(PoolType.VelodromeV2), - address(0), - uint8(SwapDirection.Token1ToToken0), - USER_SENDER, - uint8(CallbackStatus.Disabled) - ); - - IERC20(ADDRESS_USDC).approve(address(liFiDEXAggregator), 1000 * 1e6); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - ADDRESS_USDC, - 1000 * 1e6, - address(STG_TOKEN), - 0, - USER_SENDER, - routeWithZeroPool - ); - - // Test case 2: Zero recipient address - bytes memory routeWithZeroRecipient = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - ADDRESS_USDC, - uint8(1), - FULL_SHARE, - uint8(PoolType.VelodromeV2), - validPool, - uint8(SwapDirection.Token1ToToken0), - address(0), - uint8(CallbackStatus.Disabled) - ); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - ADDRESS_USDC, - 1000 * 1e6, - address(STG_TOKEN), - 0, - USER_SENDER, - routeWithZeroRecipient - ); - - vm.stopPrank(); - } - - function testRevert_WrongPoolReserves() public { - vm.startPrank(USER_SENDER); - - // Setup multi-hop route: USDC -> STG -> USDC.e - MultiHopTestParams memory params = _setupRoutes( - ADDRESS_USDC, - address(STG_TOKEN), - address(USDC_E_TOKEN), - false, - false - ); - - // Build multi-hop route - bytes memory firstHop = _buildFirstHop( - params.tokenIn, - params.pool1, - params.pool2, - 1 // direction - ); - - bytes memory secondHop = _buildSecondHop( - params.tokenMid, - params.pool2, - USER_SENDER, - 0 // direction - ); - - bytes memory route = bytes.concat(firstHop, secondHop); - - deal(ADDRESS_USDC, USER_SENDER, 1000 * 1e6); - - IERC20(ADDRESS_USDC).approve(address(liFiDEXAggregator), 1000 * 1e6); - - // Mock getReserves for the second pool (which uses processOnePool) to return zero reserves - vm.mockCall( - params.pool2, - abi.encodeWithSelector(IVelodromeV2Pool.getReserves.selector), - abi.encode(0, 0, block.timestamp) - ); - - vm.expectRevert(WrongPoolReserves.selector); - - liFiDEXAggregator.processRoute( - ADDRESS_USDC, - 1000 * 1e6, - address(USDC_E_TOKEN), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - vm.clearMockedCalls(); - } - - // ============================ Velodrome V2 Helper Functions ============================ - - /** - * @dev Helper function to test a VelodromeV2 swap. - * Uses a struct to group parameters and reduce stack depth. - */ - function _testSwap(VelodromeV2SwapTestParams memory params) internal { - // get expected output amounts from the router. - IVelodromeV2Router.Route[] - memory routes = new IVelodromeV2Router.Route[](1); - routes[0] = IVelodromeV2Router.Route({ - from: params.tokenIn, - to: params.tokenOut, - stable: params.stable, - factory: address(VELODROME_V2_FACTORY_REGISTRY) - }); - uint256[] memory amounts = VELODROME_V2_ROUTER.getAmountsOut( - params.amountIn, - routes - ); - emit log_named_uint("Expected amount out", amounts[1]); - - // Retrieve the pool address. - address pool = VELODROME_V2_ROUTER.poolFor( - params.tokenIn, - params.tokenOut, - params.stable, - VELODROME_V2_FACTORY_REGISTRY - ); - emit log_named_uint("Pool address:", uint256(uint160(pool))); - - // if tokens come from the aggregator (address(liFiDEXAggregator)), use command code 1; otherwise, use 2. - CommandType commandCode = params.from == address(liFiDEXAggregator) - ? CommandType.ProcessMyERC20 - : CommandType.ProcessUserERC20; - - // build the route. - bytes memory route = abi.encodePacked( - uint8(commandCode), - params.tokenIn, - uint8(1), - FULL_SHARE, - uint8(PoolType.VelodromeV2), - pool, - params.direction, - params.to, - params.callback - ? uint8(CallbackStatus.Enabled) - : uint8(CallbackStatus.Disabled) - ); - - // approve the aggregator to spend tokenIn. - IERC20(params.tokenIn).approve( - address(liFiDEXAggregator), - params.amountIn - ); - - // capture initial token balances. - uint256 initialTokenIn = IERC20(params.tokenIn).balanceOf(params.from); - uint256 initialTokenOut = IERC20(params.tokenOut).balanceOf(params.to); - emit log_named_uint("Initial tokenIn balance", initialTokenIn); - - address from = params.from == address(liFiDEXAggregator) - ? USER_SENDER - : params.from; - if (params.callback == true) { - vm.expectEmit(true, false, false, false); - emit HookCalled( - address(liFiDEXAggregator), - 0, - 0, - abi.encode(params.tokenIn) - ); - } - vm.expectEmit(true, true, true, true); - emit Route( - from, - params.to, - params.tokenIn, - params.tokenOut, - params.amountIn, - amounts[1], - amounts[1] - ); - - // execute the swap - liFiDEXAggregator.processRoute( - params.tokenIn, - params.amountIn, - params.tokenOut, - amounts[1], - params.to, - route - ); - - uint256 finalTokenIn = IERC20(params.tokenIn).balanceOf(params.from); - uint256 finalTokenOut = IERC20(params.tokenOut).balanceOf(params.to); - emit log_named_uint("TokenIn spent", initialTokenIn - finalTokenIn); - emit log_named_uint( - "TokenOut received", - finalTokenOut - initialTokenOut - ); - - assertApproxEqAbs( - initialTokenIn - finalTokenIn, - params.amountIn, - 1, // 1 wei tolerance - "TokenIn amount mismatch" - ); - assertEq( - finalTokenOut - initialTokenOut, - amounts[1], - "TokenOut amount mismatch" - ); - } - - // Helper function to set up routes and get amounts - function _setupRoutes( - address tokenIn, - address tokenMid, - address tokenOut, - bool isStableFirst, - bool isStableSecond - ) private view returns (MultiHopTestParams memory params) { - params.tokenIn = tokenIn; - params.tokenMid = tokenMid; - params.tokenOut = tokenOut; - - // Setup first hop route - IVelodromeV2Router.Route[] - memory routes1 = new IVelodromeV2Router.Route[](1); - routes1[0] = IVelodromeV2Router.Route({ - from: tokenIn, - to: tokenMid, - stable: isStableFirst, - factory: address(VELODROME_V2_FACTORY_REGISTRY) - }); - params.amounts1 = VELODROME_V2_ROUTER.getAmountsOut( - 1000 * 1e6, - routes1 - ); - - // Setup second hop route - IVelodromeV2Router.Route[] - memory routes2 = new IVelodromeV2Router.Route[](1); - routes2[0] = IVelodromeV2Router.Route({ - from: tokenMid, - to: tokenOut, - stable: isStableSecond, - factory: address(VELODROME_V2_FACTORY_REGISTRY) - }); - params.amounts2 = VELODROME_V2_ROUTER.getAmountsOut( - params.amounts1[1], - routes2 - ); - - // Get pool addresses - params.pool1 = VELODROME_V2_ROUTER.poolFor( - tokenIn, - tokenMid, - isStableFirst, - VELODROME_V2_FACTORY_REGISTRY - ); - - params.pool2 = VELODROME_V2_ROUTER.poolFor( - tokenMid, - tokenOut, - isStableSecond, - VELODROME_V2_FACTORY_REGISTRY - ); - - // Get pool fees info - params.pool1Fee = IVelodromeV2PoolFactory( - VELODROME_V2_FACTORY_REGISTRY - ).getFee(params.pool1, isStableFirst); - params.pool2Fee = IVelodromeV2PoolFactory( - VELODROME_V2_FACTORY_REGISTRY - ).getFee(params.pool2, isStableSecond); - - return params; - } - - // function to build first hop of the route - function _buildFirstHop( - address tokenIn, - address pool1, - address pool2, - uint8 direction - ) private pure returns (bytes memory) { - return - abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - tokenIn, - uint8(1), - FULL_SHARE, - uint8(PoolType.VelodromeV2), - pool1, - direction, - pool2, - uint8(CallbackStatus.Disabled) - ); - } - - // function to build second hop of the route - function _buildSecondHop( - address tokenMid, - address pool2, - address recipient, - uint8 direction - ) private pure returns (bytes memory) { - return - abi.encodePacked( - uint8(CommandType.ProcessOnePool), - tokenMid, - uint8(PoolType.VelodromeV2), - pool2, - direction, - recipient, - uint8(CallbackStatus.Disabled) - ); - } - - // route building function - function _buildMultiHopRoute( - MultiHopTestParams memory params, - address recipient, - uint8 firstHopDirection, - uint8 secondHopDirection - ) private pure returns (bytes memory) { - bytes memory firstHop = _buildFirstHop( - params.tokenIn, - params.pool1, - params.pool2, - firstHopDirection - ); - - bytes memory secondHop = _buildSecondHop( - params.tokenMid, - params.pool2, - recipient, - secondHopDirection - ); - - return bytes.concat(firstHop, secondHop); - } - - function _verifyUserBalances( - MultiHopTestParams memory params, - uint256 initialBalance1, - uint256 initialBalance2 - ) private { - // Verify token balances - uint256 finalBalance1 = IERC20(params.tokenIn).balanceOf(USER_SENDER); - uint256 finalBalance2 = IERC20(params.tokenOut).balanceOf(USER_SENDER); - - assertApproxEqAbs( - initialBalance1 - finalBalance1, - 1000 * 1e6, - 1, // 1 wei tolerance - "Token1 spent amount mismatch" - ); - assertEq( - finalBalance2 - initialBalance2, - params.amounts2[1], - "Token2 received amount mismatch" - ); - } - - function _verifyReserves( - MultiHopTestParams memory params, - ReserveState memory initialReserves - ) private { - // Get reserves after swap - ( - uint256 finalReserve0Pool1, - uint256 finalReserve1Pool1, - - ) = IVelodromeV2Pool(params.pool1).getReserves(); - ( - uint256 finalReserve0Pool2, - uint256 finalReserve1Pool2, - - ) = IVelodromeV2Pool(params.pool2).getReserves(); - - address token0Pool1 = IVelodromeV2Pool(params.pool1).token0(); - address token0Pool2 = IVelodromeV2Pool(params.pool2).token0(); - - // Calculate exact expected changes - uint256 amountInAfterFees = 1000 * - 1e6 - - ((1000 * 1e6 * params.pool1Fee) / 10000); - - // Assert exact reserve changes for Pool1 - if (token0Pool1 == params.tokenIn) { - // tokenIn is token0, so reserve0 should increase and reserve1 should decrease - assertEq( - finalReserve0Pool1 - initialReserves.reserve0Pool1, - amountInAfterFees, - "Pool1 reserve0 (tokenIn) change incorrect" - ); - assertEq( - initialReserves.reserve1Pool1 - finalReserve1Pool1, - params.amounts1[1], - "Pool1 reserve1 (tokenMid) change incorrect" - ); - } else { - // tokenIn is token1, so reserve1 should increase and reserve0 should decrease - assertEq( - finalReserve1Pool1 - initialReserves.reserve1Pool1, - amountInAfterFees, - "Pool1 reserve1 (tokenIn) change incorrect" - ); - assertEq( - initialReserves.reserve0Pool1 - finalReserve0Pool1, - params.amounts1[1], - "Pool1 reserve0 (tokenMid) change incorrect" - ); - } - - // Assert exact reserve changes for Pool2 - if (token0Pool2 == params.tokenMid) { - // tokenMid is token0, so reserve0 should increase and reserve1 should decrease - assertEq( - finalReserve0Pool2 - initialReserves.reserve0Pool2, - params.amounts1[1] - - ((params.amounts1[1] * params.pool2Fee) / 10000), - "Pool2 reserve0 (tokenMid) change incorrect" - ); - assertEq( - initialReserves.reserve1Pool2 - finalReserve1Pool2, - params.amounts2[1], - "Pool2 reserve1 (tokenOut) change incorrect" - ); - } else { - // tokenMid is token1, so reserve1 should increase and reserve0 should decrease - assertEq( - finalReserve1Pool2 - initialReserves.reserve1Pool2, - params.amounts1[1] - - ((params.amounts1[1] * params.pool2Fee) / 10000), - "Pool2 reserve1 (tokenMid) change incorrect" - ); - assertEq( - initialReserves.reserve0Pool2 - finalReserve0Pool2, - params.amounts2[1], - "Pool2 reserve0 (tokenOut) change incorrect" - ); - } - } -} - -contract AlgebraLiquidityAdderHelper { - address public immutable TOKEN_0; - address public immutable TOKEN_1; - - constructor(address _token0, address _token1) { - TOKEN_0 = _token0; - TOKEN_1 = _token1; - } - - function addLiquidity( - address pool, - int24 bottomTick, - int24 topTick, - uint128 amount - ) - external - returns (uint256 amount0, uint256 amount1, uint128 liquidityActual) - { - // Get balances before - uint256 balance0Before = IERC20(TOKEN_0).balanceOf(address(this)); - uint256 balance1Before = IERC20(TOKEN_1).balanceOf(address(this)); - - // Call mint - (amount0, amount1, liquidityActual) = IAlgebraPool(pool).mint( - address(this), - address(this), - bottomTick, - topTick, - amount, - abi.encode(TOKEN_0, TOKEN_1) - ); - - // Get balances after to account for fees - uint256 balance0After = IERC20(TOKEN_0).balanceOf(address(this)); - uint256 balance1After = IERC20(TOKEN_1).balanceOf(address(this)); - - // Calculate actual amounts transferred accounting for fees - amount0 = balance0Before - balance0After; - amount1 = balance1Before - balance1After; - - return (amount0, amount1, liquidityActual); - } - - function algebraMintCallback( - uint256 amount0Owed, - uint256 amount1Owed, - bytes calldata - ) external { - // Check token balances - uint256 balance0 = IERC20(TOKEN_0).balanceOf(address(this)); - uint256 balance1 = IERC20(TOKEN_1).balanceOf(address(this)); - - // Transfer what we can, limited by actual balance - if (amount0Owed > 0) { - uint256 amount0ToSend = amount0Owed > balance0 - ? balance0 - : amount0Owed; - uint256 balance0Before = IERC20(TOKEN_0).balanceOf( - address(msg.sender) - ); - IERC20(TOKEN_0).transfer(msg.sender, amount0ToSend); - uint256 balance0After = IERC20(TOKEN_0).balanceOf( - address(msg.sender) - ); - // solhint-disable-next-line gas-custom-errors - require(balance0After > balance0Before, "Transfer failed"); - } - - if (amount1Owed > 0) { - uint256 amount1ToSend = amount1Owed > balance1 - ? balance1 - : amount1Owed; - uint256 balance1Before = IERC20(TOKEN_1).balanceOf( - address(msg.sender) - ); - IERC20(TOKEN_1).transfer(msg.sender, amount1ToSend); - uint256 balance1After = IERC20(TOKEN_1).balanceOf( - address(msg.sender) - ); - // solhint-disable-next-line gas-custom-errors - require(balance1After > balance1Before, "Transfer failed"); - } - } -} - -/** - * @title Algebra tests - * @notice Tests specific to Algebra pool type - */ -contract LiFiDexAggregatorAlgebraTest is LiFiDexAggregatorTest { - address private constant APE_ETH_TOKEN = - 0xcF800F4948D16F23333508191B1B1591daF70438; - address private constant WETH_TOKEN = - 0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949; - address private constant ALGEBRA_FACTORY_APECHAIN = - 0x10aA510d94E094Bd643677bd2964c3EE085Daffc; - address private constant ALGEBRA_QUOTER_V2_APECHAIN = - 0x60A186019F81bFD04aFc16c9C01804a04E79e68B; - address private constant ALGEBRA_POOL_APECHAIN = - 0x217076aa74eFF7D54837D00296e9AEBc8c06d4F2; - address private constant APE_ETH_HOLDER_APECHAIN = - address(0x1EA5Df273F1b2e0b10554C8F6f7Cc7Ef34F6a51b); - - address private constant IMPOSSIBLE_POOL_ADDRESS = - 0x0000000000000000000000000000000000000001; - - struct AlgebraSwapTestParams { - address from; - address to; - address tokenIn; - uint256 amountIn; - address tokenOut; - SwapDirection direction; - bool supportsFeeOnTransfer; - } - - error AlgebraSwapUnexpected(); - - function setUp() public override { - setupApechain(); - } - - // Override the abstract test with Algebra implementation - function test_CanSwap_FromDexAggregator() public override { - // Fund LDA from whale address - vm.prank(APE_ETH_HOLDER_APECHAIN); - IERC20(APE_ETH_TOKEN).transfer(address(liFiDEXAggregator), 1 * 1e18); - - vm.startPrank(USER_SENDER); - - _testAlgebraSwap( - AlgebraSwapTestParams({ - from: address(liFiDEXAggregator), - to: address(USER_SENDER), - tokenIn: APE_ETH_TOKEN, - amountIn: IERC20(APE_ETH_TOKEN).balanceOf( - address(liFiDEXAggregator) - ) - 1, - tokenOut: address(WETH_TOKEN), - direction: SwapDirection.Token0ToToken1, - supportsFeeOnTransfer: true - }) - ); - - vm.stopPrank(); - } - - function test_CanSwap_FeeOnTransferToken() public { - setupApechain(); - - uint256 amountIn = 534451326669177; - vm.prank(APE_ETH_HOLDER_APECHAIN); - IERC20(APE_ETH_TOKEN).transfer(APE_ETH_HOLDER_APECHAIN, amountIn); - - vm.startPrank(APE_ETH_HOLDER_APECHAIN); - - IERC20(APE_ETH_TOKEN).approve(address(liFiDEXAggregator), amountIn); - - // Build route for algebra swap with command code 2 (user funds) - bytes memory route = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessUserERC20, - tokenIn: APE_ETH_TOKEN, - recipient: APE_ETH_HOLDER_APECHAIN, - pool: ALGEBRA_POOL_APECHAIN, - supportsFeeOnTransfer: true - }) - ); - - // Track initial balance - uint256 beforeBalance = IERC20(WETH_TOKEN).balanceOf( - APE_ETH_HOLDER_APECHAIN - ); - - // Execute the swap - liFiDEXAggregator.processRoute( - APE_ETH_TOKEN, - amountIn, - WETH_TOKEN, - 0, // minOut = 0 for this test - APE_ETH_HOLDER_APECHAIN, - route - ); - - // Verify balances - uint256 afterBalance = IERC20(WETH_TOKEN).balanceOf( - APE_ETH_HOLDER_APECHAIN - ); - assertGt(afterBalance - beforeBalance, 0, "Should receive some WETH"); - - vm.stopPrank(); - } - - function test_CanSwap() public override { - vm.startPrank(APE_ETH_HOLDER_APECHAIN); - - // Transfer tokens from whale to USER_SENDER - uint256 amountToTransfer = 100 * 1e18; - IERC20(APE_ETH_TOKEN).transfer(USER_SENDER, amountToTransfer); - - vm.stopPrank(); - - vm.startPrank(USER_SENDER); - - _testAlgebraSwap( - AlgebraSwapTestParams({ - from: USER_SENDER, - to: USER_SENDER, - tokenIn: APE_ETH_TOKEN, - amountIn: 10 * 1e18, - tokenOut: address(WETH_TOKEN), - direction: SwapDirection.Token0ToToken1, - supportsFeeOnTransfer: true - }) - ); - - vm.stopPrank(); - } - - function test_CanSwap_Reverse() public { - test_CanSwap(); - - vm.startPrank(USER_SENDER); - - _testAlgebraSwap( - AlgebraSwapTestParams({ - from: USER_SENDER, - to: USER_SENDER, - tokenIn: address(WETH_TOKEN), - amountIn: 5 * 1e18, - tokenOut: APE_ETH_TOKEN, - direction: SwapDirection.Token1ToToken0, - supportsFeeOnTransfer: false - }) - ); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop_WithFeeOnTransferToken() public { - MultiHopTestState memory state; - state.isFeeOnTransfer = true; - - // Setup tokens and pools - state = _setupTokensAndPools(state); - - // Execute and verify swap - _executeAndVerifyMultiHopSwap(state); - } - - function test_CanSwap_MultiHop() public override { - MultiHopTestState memory state; - state.isFeeOnTransfer = false; - - // Setup tokens and pools - state = _setupTokensAndPools(state); - - // Execute and verify swap - _executeAndVerifyMultiHopSwap(state); - } - - // Test that the proper error is thrown when algebra swap fails - function testRevert_SwapUnexpected() public { - // Transfer tokens from whale to user - vm.prank(APE_ETH_HOLDER_APECHAIN); - IERC20(APE_ETH_TOKEN).transfer(USER_SENDER, 1 * 1e18); - - vm.startPrank(USER_SENDER); - - // Create invalid pool address - address invalidPool = address(0x999); - - // Mock token0() call on invalid pool - vm.mockCall( - invalidPool, - abi.encodeWithSelector(IAlgebraPool.token0.selector), - abi.encode(APE_ETH_TOKEN) - ); - - // Create a route with an invalid pool - bytes memory invalidRoute = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessUserERC20, - tokenIn: APE_ETH_TOKEN, - recipient: USER_SENDER, - pool: invalidPool, - supportsFeeOnTransfer: true - }) - ); - - // Approve tokens - IERC20(APE_ETH_TOKEN).approve(address(liFiDEXAggregator), 1 * 1e18); - - // Mock the algebra pool to not reset lastCalledPool - vm.mockCall( - invalidPool, - abi.encodeWithSelector( - IAlgebraPool.swapSupportingFeeOnInputTokens.selector - ), - abi.encode(0, 0) - ); - - // Expect the AlgebraSwapUnexpected error - vm.expectRevert(AlgebraSwapUnexpected.selector); - - liFiDEXAggregator.processRoute( - APE_ETH_TOKEN, - 1 * 1e18, - address(WETH_TOKEN), - 0, - USER_SENDER, - invalidRoute - ); - - vm.stopPrank(); - vm.clearMockedCalls(); - } - - // Helper function to setup tokens and pools - function _setupTokensAndPools( - MultiHopTestState memory state - ) private returns (MultiHopTestState memory) { - // Create tokens - ERC20 tokenA = new ERC20( - "Token A", - state.isFeeOnTransfer ? "FTA" : "TA", - 18 - ); - IERC20 tokenB; - ERC20 tokenC = new ERC20( - "Token C", - state.isFeeOnTransfer ? "FTC" : "TC", - 18 - ); - - if (state.isFeeOnTransfer) { - tokenB = IERC20( - address( - new MockFeeOnTransferToken("Fee Token B", "FTB", 18, 300) - ) - ); - } else { - tokenB = IERC20(address(new ERC20("Token B", "TB", 18))); - } - - state.tokenA = IERC20(address(tokenA)); - state.tokenB = tokenB; - state.tokenC = IERC20(address(tokenC)); - - // Label addresses - vm.label(address(state.tokenA), "Token A"); - vm.label(address(state.tokenB), "Token B"); - vm.label(address(state.tokenC), "Token C"); - - // Mint initial token supplies - tokenA.mint(address(this), 1_000_000 * 1e18); - if (!state.isFeeOnTransfer) { - ERC20(address(tokenB)).mint(address(this), 1_000_000 * 1e18); - } else { - MockFeeOnTransferToken(address(tokenB)).mint( - address(this), - 1_000_000 * 1e18 - ); - } - tokenC.mint(address(this), 1_000_000 * 1e18); - - // Create pools - state.pool1 = _createAlgebraPool( - address(state.tokenA), - address(state.tokenB) - ); - state.pool2 = _createAlgebraPool( - address(state.tokenB), - address(state.tokenC) - ); - - vm.label(state.pool1, "Pool 1"); - vm.label(state.pool2, "Pool 2"); - - // Add liquidity - _addLiquidityToPool( - state.pool1, - address(state.tokenA), - address(state.tokenB) - ); - _addLiquidityToPool( - state.pool2, - address(state.tokenB), - address(state.tokenC) - ); - - state.amountToTransfer = 100 * 1e18; - state.amountIn = 50 * 1e18; - - // Transfer tokens to USER_SENDER - IERC20(address(state.tokenA)).transfer( - USER_SENDER, - state.amountToTransfer - ); - - return state; - } - - // Helper function to execute and verify the swap - function _executeAndVerifyMultiHopSwap( - MultiHopTestState memory state - ) private { - vm.startPrank(USER_SENDER); - - uint256 initialBalanceA = IERC20(address(state.tokenA)).balanceOf( - USER_SENDER - ); - uint256 initialBalanceC = IERC20(address(state.tokenC)).balanceOf( - USER_SENDER - ); - - // Approve spending - IERC20(address(state.tokenA)).approve( - address(liFiDEXAggregator), - state.amountIn - ); - - // Build route - bytes memory route = _buildMultiHopRouteForTest(state); - - // Execute swap - liFiDEXAggregator.processRoute( - address(state.tokenA), - state.amountIn, - address(state.tokenC), - 0, // No minimum amount out for testing - USER_SENDER, - route - ); - - // Verify results - _verifyMultiHopResults(state, initialBalanceA, initialBalanceC); - - vm.stopPrank(); - } - - // Helper function to build the multi-hop route for test - function _buildMultiHopRouteForTest( - MultiHopTestState memory state - ) private view returns (bytes memory) { - bytes memory firstHop = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessUserERC20, - tokenIn: address(state.tokenA), - recipient: address(liFiDEXAggregator), - pool: state.pool1, - supportsFeeOnTransfer: false - }) - ); - - bytes memory secondHop = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessMyERC20, - tokenIn: address(state.tokenB), - recipient: USER_SENDER, - pool: state.pool2, - supportsFeeOnTransfer: state.isFeeOnTransfer - }) - ); - - return bytes.concat(firstHop, secondHop); - } - - // Helper function to verify multi-hop results - function _verifyMultiHopResults( - MultiHopTestState memory state, - uint256 initialBalanceA, - uint256 initialBalanceC - ) private { - uint256 finalBalanceA = IERC20(address(state.tokenA)).balanceOf( - USER_SENDER - ); - uint256 finalBalanceC = IERC20(address(state.tokenC)).balanceOf( - USER_SENDER - ); - - assertApproxEqAbs( - initialBalanceA - finalBalanceA, - state.amountIn, - 1, // 1 wei tolerance - "TokenA spent amount mismatch" - ); - assertGt(finalBalanceC, initialBalanceC, "TokenC not received"); - - emit log_named_uint( - state.isFeeOnTransfer - ? "Output amount with fee tokens" - : "Output amount with regular tokens", - finalBalanceC - initialBalanceC - ); - } - - // Helper function to create an Algebra pool - function _createAlgebraPool( - address tokenA, - address tokenB - ) internal returns (address pool) { - // Call the actual Algebra factory to create a pool - pool = IAlgebraFactory(ALGEBRA_FACTORY_APECHAIN).createPool( - tokenA, - tokenB - ); - return pool; - } - - // Helper function to add liquidity to a pool - function _addLiquidityToPool( - address pool, - address token0, - address token1 - ) internal { - // For fee-on-transfer tokens, we need to send more to account for the fee - // We'll use a small amount and send extra to cover fees - uint256 initialAmount0 = 1e17; // 0.1 token - uint256 initialAmount1 = 1e17; // 0.1 token - - // Send extra for fee-on-transfer tokens (10% extra should be enough for our test tokens with 5% fee) - uint256 transferAmount0 = (initialAmount0 * 110) / 100; - uint256 transferAmount1 = (initialAmount1 * 110) / 100; - - // Initialize with 1:1 price ratio (Q64.96 format) - uint160 initialPrice = uint160(1 << 96); - IAlgebraPool(pool).initialize(initialPrice); - - // Create AlgebraLiquidityAdderHelper with safe transfer logic - AlgebraLiquidityAdderHelper algebraLiquidityAdderHelper = new AlgebraLiquidityAdderHelper( - token0, - token1 - ); - - // Transfer tokens with extra amounts to account for fees - IERC20(token0).transfer( - address(algebraLiquidityAdderHelper), - transferAmount0 - ); - IERC20(token1).transfer( - address(algebraLiquidityAdderHelper), - transferAmount1 - ); - - // Get actual balances to use for liquidity, accounting for any fees - uint256 actualBalance0 = IERC20(token0).balanceOf( - address(algebraLiquidityAdderHelper) - ); - uint256 actualBalance1 = IERC20(token1).balanceOf( - address(algebraLiquidityAdderHelper) - ); - - // Use the smaller of the two balances for liquidity amount - uint128 liquidityAmount = uint128( - actualBalance0 < actualBalance1 ? actualBalance0 : actualBalance1 - ); - - // Add liquidity using the actual token amounts we have - algebraLiquidityAdderHelper.addLiquidity( - pool, - -887220, - 887220, - liquidityAmount / 2 // Use half of available liquidity to ensure success - ); - } - - struct MultiHopTestState { - IERC20 tokenA; - IERC20 tokenB; // Can be either regular ERC20 or MockFeeOnTransferToken - IERC20 tokenC; - address pool1; - address pool2; - uint256 amountIn; - uint256 amountToTransfer; - bool isFeeOnTransfer; - } - - struct AlgebraRouteParams { - CommandType commandCode; // 1 for contract funds, 2 for user funds - address tokenIn; // Input token address - address recipient; // Address receiving the output tokens - address pool; // Algebra pool address - bool supportsFeeOnTransfer; // Whether to support fee-on-transfer tokens - } - - // Helper function to build route for Apechain Algebra swap - function _buildAlgebraRoute( - AlgebraRouteParams memory params - ) internal view returns (bytes memory route) { - address token0 = IAlgebraPool(params.pool).token0(); - bool zeroForOne = (params.tokenIn == token0); - SwapDirection direction = zeroForOne - ? SwapDirection.Token0ToToken1 - : SwapDirection.Token1ToToken0; - - route = abi.encodePacked( - params.commandCode, - params.tokenIn, - uint8(1), // one pool - FULL_SHARE, // 100% share - uint8(PoolType.Algebra), - params.pool, - uint8(direction), - params.recipient, - params.supportsFeeOnTransfer ? uint8(1) : uint8(0) - ); - - return route; - } - - // Helper function to test an Algebra swap - function _testAlgebraSwap(AlgebraSwapTestParams memory params) internal { - // Find or create a pool - address pool = _getPool(params.tokenIn, params.tokenOut); - - vm.label(pool, "AlgebraPool"); - - // Get token0 from pool and label tokens accordingly - address token0 = IAlgebraPool(pool).token0(); - if (params.tokenIn == token0) { - vm.label( - params.tokenIn, - string.concat("token0 (", ERC20(params.tokenIn).symbol(), ")") - ); - vm.label( - params.tokenOut, - string.concat("token1 (", ERC20(params.tokenOut).symbol(), ")") - ); - } else { - vm.label( - params.tokenIn, - string.concat("token1 (", ERC20(params.tokenIn).symbol(), ")") - ); - vm.label( - params.tokenOut, - string.concat("token0 (", ERC20(params.tokenOut).symbol(), ")") - ); - } - - // Record initial balances - uint256 initialTokenIn = IERC20(params.tokenIn).balanceOf(params.from); - uint256 initialTokenOut = IERC20(params.tokenOut).balanceOf(params.to); - - // Get expected output from QuoterV2 - // NOTE: There may be a small discrepancy between the quoted amount and the actual output - // because the Quoter uses the regular swap() function for simulation while the actual - // execution may use swapSupportingFeeOnInputTokens() for fee-on-transfer tokens. - // The Quoter cannot accurately predict transfer fees taken by the token contract itself, - // resulting in minor "dust" differences that are normal and expected when dealing with - // non-standard token implementations. - uint256 expectedOutput = _getQuoteExactInput( - params.tokenIn, - params.tokenOut, - params.amountIn - ); - - // Build the route - CommandType commandCode = params.from == address(liFiDEXAggregator) - ? CommandType.ProcessMyERC20 - : CommandType.ProcessUserERC20; - bytes memory route = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: commandCode, - tokenIn: params.tokenIn, - recipient: params.to, - pool: pool, - supportsFeeOnTransfer: params.supportsFeeOnTransfer - }) - ); - - // Approve tokens - IERC20(params.tokenIn).approve( - address(liFiDEXAggregator), - params.amountIn - ); - - // Execute the swap - address from = params.from == address(liFiDEXAggregator) - ? USER_SENDER - : params.from; - - vm.expectEmit(true, true, true, false); - emit Route( - from, - params.to, - params.tokenIn, - params.tokenOut, - params.amountIn, - expectedOutput, - expectedOutput - ); - - uint256 minOut = (expectedOutput * 995) / 1000; // 0.5% slippage - - liFiDEXAggregator.processRoute( - params.tokenIn, - params.amountIn, - params.tokenOut, - minOut, - params.to, - route - ); - - uint256 finalTokenIn = IERC20(params.tokenIn).balanceOf(params.from); - uint256 finalTokenOut = IERC20(params.tokenOut).balanceOf(params.to); - - assertApproxEqAbs( - initialTokenIn - finalTokenIn, - params.amountIn, - 1, // 1 wei tolerance - "TokenIn amount mismatch" - ); - assertGt(finalTokenOut, initialTokenOut, "TokenOut not received"); - } - - function _getPool( - address tokenA, - address tokenB - ) private view returns (address pool) { - pool = IAlgebraRouter(ALGEBRA_FACTORY_APECHAIN).poolByPair( - tokenA, - tokenB - ); - if (pool == address(0)) revert PoolDoesNotExist(); - return pool; - } - - function _getQuoteExactInput( - address tokenIn, - address tokenOut, - uint256 amountIn - ) private returns (uint256 amountOut) { - (amountOut, ) = IAlgebraQuoter(ALGEBRA_QUOTER_V2_APECHAIN) - .quoteExactInputSingle(tokenIn, tokenOut, amountIn, 0); - return amountOut; - } - - function testRevert_AlgebraSwap_ZeroAddressPool() public { - // Transfer tokens from whale to user - vm.prank(APE_ETH_HOLDER_APECHAIN); - IERC20(APE_ETH_TOKEN).transfer(USER_SENDER, 1 * 1e18); - - vm.startPrank(USER_SENDER); - - // Mock token0() call on address(0) - vm.mockCall( - address(0), - abi.encodeWithSelector(IAlgebraPool.token0.selector), - abi.encode(APE_ETH_TOKEN) - ); - - // Build route with address(0) as pool - bytes memory route = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessUserERC20, - tokenIn: APE_ETH_TOKEN, - recipient: USER_SENDER, - pool: address(0), // Zero address pool - supportsFeeOnTransfer: true - }) - ); - - // Approve tokens - IERC20(APE_ETH_TOKEN).approve(address(liFiDEXAggregator), 1 * 1e18); - - // Expect revert with InvalidCallData - vm.expectRevert(InvalidCallData.selector); - - liFiDEXAggregator.processRoute( - APE_ETH_TOKEN, - 1 * 1e18, - address(WETH_TOKEN), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - vm.clearMockedCalls(); - } - - function testRevert_AlgebraSwap_ImpossiblePoolAddress() public { - // Transfer tokens from whale to user - vm.prank(APE_ETH_HOLDER_APECHAIN); - IERC20(APE_ETH_TOKEN).transfer(USER_SENDER, 1 * 1e18); - - vm.startPrank(USER_SENDER); - - // Mock token0() call on IMPOSSIBLE_POOL_ADDRESS - vm.mockCall( - IMPOSSIBLE_POOL_ADDRESS, - abi.encodeWithSelector(IAlgebraPool.token0.selector), - abi.encode(APE_ETH_TOKEN) - ); - - // Build route with IMPOSSIBLE_POOL_ADDRESS as pool - bytes memory route = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessUserERC20, - tokenIn: APE_ETH_TOKEN, - recipient: USER_SENDER, - pool: IMPOSSIBLE_POOL_ADDRESS, // Impossible pool address - supportsFeeOnTransfer: true - }) - ); - - // Approve tokens - IERC20(APE_ETH_TOKEN).approve(address(liFiDEXAggregator), 1 * 1e18); - - // Expect revert with InvalidCallData - vm.expectRevert(InvalidCallData.selector); - - liFiDEXAggregator.processRoute( - APE_ETH_TOKEN, - 1 * 1e18, - address(WETH_TOKEN), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - vm.clearMockedCalls(); - } - - function testRevert_AlgebraSwap_ZeroAddressRecipient() public { - // Transfer tokens from whale to user - vm.prank(APE_ETH_HOLDER_APECHAIN); - IERC20(APE_ETH_TOKEN).transfer(USER_SENDER, 1 * 1e18); - - vm.startPrank(USER_SENDER); - - // Mock token0() call on the pool - vm.mockCall( - ALGEBRA_POOL_APECHAIN, - abi.encodeWithSelector(IAlgebraPool.token0.selector), - abi.encode(APE_ETH_TOKEN) - ); - - // Build route with address(0) as recipient - bytes memory route = _buildAlgebraRoute( - AlgebraRouteParams({ - commandCode: CommandType.ProcessUserERC20, - tokenIn: APE_ETH_TOKEN, - recipient: address(0), // Zero address recipient - pool: ALGEBRA_POOL_APECHAIN, - supportsFeeOnTransfer: true - }) - ); - - // Approve tokens - IERC20(APE_ETH_TOKEN).approve(address(liFiDEXAggregator), 1 * 1e18); - - // Expect revert with InvalidCallData - vm.expectRevert(InvalidCallData.selector); - - liFiDEXAggregator.processRoute( - APE_ETH_TOKEN, - 1 * 1e18, - address(WETH_TOKEN), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - vm.clearMockedCalls(); - } -} - -/** - * @title LiFiDexAggregatorIzumiV3Test - * @notice Tests specific to iZiSwap V3 pool type - */ -contract LiFiDexAggregatorIzumiV3Test is LiFiDexAggregatorTest { - // ==================== iZiSwap V3 specific variables ==================== - // Base constants - address internal constant USDC = - 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; - address internal constant WETH = - 0x4200000000000000000000000000000000000006; - address internal constant USDB_C = - 0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA; - - // iZiSwap pools - address internal constant IZUMI_WETH_USDC_POOL = - 0xb92A9A91a9F7E8e6Bb848508A6DaF08f9D718554; - address internal constant IZUMI_WETH_USDB_C_POOL = - 0xdb5D62f06EEcEf0Da7506e0700c2f03c57016De5; - - // Test parameters - uint256 internal constant AMOUNT_USDC = 100 * 1e6; // 100 USDC with 6 decimals - uint256 internal constant AMOUNT_WETH = 1 * 1e18; // 1 WETH with 18 decimals - - // structs - struct IzumiV3SwapTestParams { - address from; - address to; - address tokenIn; - uint256 amountIn; - address tokenOut; - SwapDirection direction; - } - - struct MultiHopTestParams { - address tokenIn; - address tokenMid; - address tokenOut; - address pool1; - address pool2; - uint256 amountIn; - SwapDirection direction1; - SwapDirection direction2; - } - - error IzumiV3SwapUnexpected(); - error IzumiV3SwapCallbackUnknownSource(); - error IzumiV3SwapCallbackNotPositiveAmount(); - - function setUp() public override { - super.setUp(); - - string memory baseRpc = vm.envString("ETH_NODE_URI_BASE"); - vm.createSelectFork(baseRpc, 29831758); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - - // Setup labels - vm.label(address(liFiDEXAggregator), "LiFiDEXAggregator"); - vm.label(USDC, "USDC"); - vm.label(WETH, "WETH"); - vm.label(USDB_C, "USDB-C"); - vm.label(IZUMI_WETH_USDC_POOL, "WETH-USDC Pool"); - vm.label(IZUMI_WETH_USDB_C_POOL, "WETH-USDB-C Pool"); - } - - function test_CanSwap_FromDexAggregator() public override { - // Test USDC -> WETH - deal(USDC, address(liFiDEXAggregator), AMOUNT_USDC); - - vm.startPrank(USER_SENDER); - _testSwap( - IzumiV3SwapTestParams({ - from: address(liFiDEXAggregator), - to: USER_SENDER, - tokenIn: USDC, - amountIn: AMOUNT_USDC, - tokenOut: WETH, - direction: SwapDirection.Token1ToToken0 - }) - ); - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - _testMultiHopSwap( - MultiHopTestParams({ - tokenIn: USDC, - tokenMid: WETH, - tokenOut: USDB_C, - pool1: IZUMI_WETH_USDC_POOL, - pool2: IZUMI_WETH_USDB_C_POOL, - amountIn: AMOUNT_USDC, - direction1: SwapDirection.Token1ToToken0, - direction2: SwapDirection.Token0ToToken1 - }) - ); - } - - function test_CanSwap() public override { - deal(address(USDC), USER_SENDER, AMOUNT_USDC); - - vm.startPrank(USER_SENDER); - IERC20(USDC).approve(address(liFiDEXAggregator), AMOUNT_USDC); - - // fix the swap data encoding - bytes memory swapData = _buildIzumiV3Route( - CommandType.ProcessUserERC20, - USDC, - uint8(SwapDirection.Token1ToToken0), - IZUMI_WETH_USDC_POOL, - USER_RECEIVER - ); - - vm.expectEmit(true, true, true, false); - emit Route(USER_SENDER, USER_RECEIVER, USDC, WETH, AMOUNT_USDC, 0, 0); - - liFiDEXAggregator.processRoute( - USDC, - AMOUNT_USDC, - WETH, - 0, - USER_RECEIVER, - swapData - ); - - vm.stopPrank(); - } - - function testRevert_IzumiV3SwapUnexpected() public { - deal(USDC, USER_SENDER, AMOUNT_USDC); - - vm.startPrank(USER_SENDER); - - // create invalid pool address - address invalidPool = address(0x999); - - // create a route with an invalid pool - bytes memory invalidRoute = _buildIzumiV3Route( - CommandType.ProcessUserERC20, - USDC, - uint8(SwapDirection.Token1ToToken0), - invalidPool, - USER_SENDER - ); - - IERC20(USDC).approve(address(liFiDEXAggregator), AMOUNT_USDC); - - // mock the iZiSwap pool to return without updating lastCalledPool - vm.mockCall( - invalidPool, - abi.encodeWithSignature("swapY2X(address,uint128,int24,bytes)"), - abi.encode(0, 0) // return amountX and amountY without triggering callback or updating lastCalledPool - ); - - vm.expectRevert(IzumiV3SwapUnexpected.selector); - - liFiDEXAggregator.processRoute( - USDC, - AMOUNT_USDC, - WETH, - 0, - USER_SENDER, - invalidRoute - ); - - vm.stopPrank(); - vm.clearMockedCalls(); - } - - function testRevert_IzumiV3SwapCallbackUnknownSource() public { - deal(USDC, USER_SENDER, AMOUNT_USDC); - - // create invalid pool address - address invalidPool = address(0x999); - - vm.prank(USER_SENDER); - IERC20(USDC).approve(address(liFiDEXAggregator), AMOUNT_USDC); - - // mock the pool to call the callback directly without setting lastCalledPool - vm.mockCall( - invalidPool, - abi.encodeWithSignature("swapY2X(address,uint128,int24,bytes)"), - abi.encode(0, 0) - ); - - // try to call the callback directly from the pool without setting lastCalledPool - vm.prank(invalidPool); - vm.expectRevert(IzumiV3SwapCallbackUnknownSource.selector); - liFiDEXAggregator.swapY2XCallback(0, AMOUNT_USDC, abi.encode(USDC)); - - vm.clearMockedCalls(); - } - - function testRevert_IzumiV3SwapCallbackNotPositiveAmount() public { - deal(USDC, USER_SENDER, AMOUNT_USDC); - - // set lastCalledPool to the pool address to pass the unknown source check - vm.store( - address(liFiDEXAggregator), - bytes32(uint256(3)), // slot for lastCalledPool - bytes32(uint256(uint160(IZUMI_WETH_USDC_POOL))) - ); - - // try to call the callback with zero amount - vm.prank(IZUMI_WETH_USDC_POOL); - vm.expectRevert(IzumiV3SwapCallbackNotPositiveAmount.selector); - liFiDEXAggregator.swapY2XCallback( - 0, - 0, // zero amount should trigger the error - abi.encode(USDC) - ); - } - - function testRevert_FailsIfAmountInIsTooLarge() public { - deal(address(WETH), USER_SENDER, type(uint256).max); - - vm.startPrank(USER_SENDER); - IERC20(WETH).approve(address(liFiDEXAggregator), type(uint256).max); - - // fix the swap data encoding - bytes memory swapData = _buildIzumiV3Route( - CommandType.ProcessUserERC20, - WETH, - uint8(SwapDirection.Token0ToToken1), - IZUMI_WETH_USDC_POOL, - USER_RECEIVER - ); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - WETH, - type(uint216).max, - USDC, - 0, - USER_RECEIVER, - swapData - ); - - vm.stopPrank(); - } - - function _testSwap(IzumiV3SwapTestParams memory params) internal { - // Fund the sender with tokens if not the contract itself - if (params.from != address(liFiDEXAggregator)) { - deal(params.tokenIn, params.from, params.amountIn); - } - - // Capture initial token balances - uint256 initialBalanceIn = IERC20(params.tokenIn).balanceOf( - params.from - ); - uint256 initialBalanceOut = IERC20(params.tokenOut).balanceOf( - params.to - ); - - // Build the route based on the command type - CommandType commandCode = params.from == address(liFiDEXAggregator) - ? CommandType.ProcessMyERC20 - : CommandType.ProcessUserERC20; - - // Construct the route - bytes memory route = _buildIzumiV3Route( - commandCode, - params.tokenIn, - uint8(params.direction == SwapDirection.Token0ToToken1 ? 1 : 0), - IZUMI_WETH_USDC_POOL, - params.to - ); - - // Approve tokens if necessary - if (params.from == USER_SENDER) { - vm.startPrank(USER_SENDER); - IERC20(params.tokenIn).approve( - address(liFiDEXAggregator), - params.amountIn - ); - } - - // Expect the Route event emission - address from = params.from == address(liFiDEXAggregator) - ? USER_SENDER - : params.from; - - vm.expectEmit(true, true, true, false); - emit Route( - from, - params.to, - params.tokenIn, - params.tokenOut, - params.amountIn, - 0, // No minimum amount enforced in test - 0 // Actual amount will be checked after the swap - ); - - // Execute the swap - uint256 amountOut = liFiDEXAggregator.processRoute( - params.tokenIn, - params.amountIn, - params.tokenOut, - 0, // No minimum amount for testing - params.to, - route - ); - - if (params.from == USER_SENDER) { - vm.stopPrank(); - } - - // Verify balances have changed correctly - uint256 finalBalanceIn = IERC20(params.tokenIn).balanceOf(params.from); - uint256 finalBalanceOut = IERC20(params.tokenOut).balanceOf(params.to); - - assertApproxEqAbs( - initialBalanceIn - finalBalanceIn, - params.amountIn, - 1, // 1 wei tolerance because of undrain protection for dex aggregator - "TokenIn amount mismatch" - ); - assertGt(finalBalanceOut, initialBalanceOut, "TokenOut not received"); - assertEq( - amountOut, - finalBalanceOut - initialBalanceOut, - "AmountOut mismatch" - ); - - emit log_named_uint("Amount In", params.amountIn); - emit log_named_uint("Amount Out", amountOut); - } - - function _testMultiHopSwap(MultiHopTestParams memory params) internal { - // Fund the sender with tokens - deal(params.tokenIn, USER_SENDER, params.amountIn); - - // Capture initial token balances - uint256 initialBalanceIn; - uint256 initialBalanceOut; - - initialBalanceIn = IERC20(params.tokenIn).balanceOf(USER_SENDER); - initialBalanceOut = IERC20(params.tokenOut).balanceOf(USER_SENDER); - - // Build multi-hop route - bytes memory route = _buildIzumiV3MultiHopRoute(params); - - // Approve tokens - vm.startPrank(USER_SENDER); - IERC20(params.tokenIn).approve( - address(liFiDEXAggregator), - params.amountIn - ); - - // Execute the swap - uint256 amountOut = liFiDEXAggregator.processRoute( - params.tokenIn, - params.amountIn, - params.tokenOut, - 0, // No minimum amount for testing - USER_SENDER, - route - ); - vm.stopPrank(); - - // Verify balances have changed correctly - uint256 finalBalanceIn; - uint256 finalBalanceOut; - - finalBalanceIn = IERC20(params.tokenIn).balanceOf(USER_SENDER); - finalBalanceOut = IERC20(params.tokenOut).balanceOf(USER_SENDER); - - assertEq( - initialBalanceIn - finalBalanceIn, - params.amountIn, - "TokenIn amount mismatch" - ); - assertGt(finalBalanceOut, initialBalanceOut, "TokenOut not received"); - assertEq( - amountOut, - finalBalanceOut - initialBalanceOut, - "AmountOut mismatch" - ); - } - - function _buildIzumiV3Route( - CommandType commandCode, - address tokenIn, - uint8 direction, - address pool, - address recipient - ) internal pure returns (bytes memory) { - return - abi.encodePacked( - uint8(commandCode), - tokenIn, - uint8(1), // number of pools (1) - FULL_SHARE, // 100% share - uint8(PoolType.iZiSwap), // pool type - pool, - uint8(direction), - recipient - ); - } - - function _buildIzumiV3MultiHopRoute( - MultiHopTestParams memory params - ) internal view returns (bytes memory) { - // First hop: USER_ERC20 -> LDA - bytes memory firstHop = _buildIzumiV3Route( - CommandType.ProcessUserERC20, - params.tokenIn, - uint8(params.direction1), - params.pool1, - address(liFiDEXAggregator) - ); - - // Second hop: MY_ERC20 (LDA) -> pool2 - bytes memory secondHop = _buildIzumiV3Route( - CommandType.ProcessMyERC20, - params.tokenMid, - uint8(params.direction2), - params.pool2, - USER_SENDER // final recipient - ); - - // Combine the two hops - return bytes.concat(firstHop, secondHop); - } -} - -// ----------------------------------------------------------------------------- -// HyperswapV3 on HyperEVM -// ----------------------------------------------------------------------------- -contract LiFiDexAggregatorHyperswapV3Test is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - /// @dev HyperswapV3 router on HyperEVM chain - IHyperswapV3Factory internal constant HYPERSWAP_FACTORY = - IHyperswapV3Factory(0xB1c0fa0B789320044A6F623cFe5eBda9562602E3); - /// @dev HyperswapV3 quoter on HyperEVM chain - IHyperswapV3QuoterV2 internal constant HYPERSWAP_QUOTER = - IHyperswapV3QuoterV2(0x03A918028f22D9E1473B7959C927AD7425A45C7C); - - /// @dev a liquid USDT on HyperEVM - IERC20 internal constant USDT0 = - IERC20(0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb); - /// @dev WHYPE on HyperEVM - IERC20 internal constant WHYPE = - IERC20(0x5555555555555555555555555555555555555555); - - struct HyperswapV3Params { - CommandType commandCode; // ProcessMyERC20 or ProcessUserERC20 - address tokenIn; // Input token address - address recipient; // Address receiving the output tokens - address pool; // HyperswapV3 pool address - bool zeroForOne; // Direction of the swap - } - - function setUp() public override { - setupHyperEVM(); - } - - function test_CanSwap() public override { - uint256 amountIn = 1_000 * 1e6; // 1000 USDT0 - - deal(address(USDT0), USER_SENDER, amountIn); - - // user approves - vm.prank(USER_SENDER); - USDT0.approve(address(liFiDEXAggregator), amountIn); - - // fetch the real pool and quote - address pool = HYPERSWAP_FACTORY.getPool( - address(USDT0), - address(WHYPE), - 3000 - ); - - // Create the params struct for quoting - IHyperswapV3QuoterV2.QuoteExactInputSingleParams - memory params = IHyperswapV3QuoterV2.QuoteExactInputSingleParams({ - tokenIn: address(USDT0), - tokenOut: address(WHYPE), - amountIn: amountIn, - fee: 3000, - sqrtPriceLimitX96: 0 - }); - - // Get the quote using the struct - (uint256 quoted, , , ) = HYPERSWAP_QUOTER.quoteExactInputSingle( - params - ); - - // build the "off-chain" route - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(USDT0), - uint8(1), // 1 pool - uint16(65535), // FULL_SHARE - uint8(1), // POOL_TYPE_UNIV3 - pool, - uint8(0), // zeroForOne = true if USDT0 < WHYPE - address(USER_SENDER) - ); - - // expect the Route event - vm.expectEmit(true, true, true, true); - emit Route( - USER_SENDER, - USER_SENDER, - address(USDT0), - address(WHYPE), - amountIn, - quoted, - quoted - ); - - // execute - vm.prank(USER_SENDER); - liFiDEXAggregator.processRoute( - address(USDT0), - amountIn, - address(WHYPE), - quoted, - USER_SENDER, - route - ); - } - - function test_CanSwap_FromDexAggregator() public override { - uint256 amountIn = 1_000 * 1e6; // 1000 USDT0 - - // Fund dex aggregator contract - deal(address(USDT0), address(liFiDEXAggregator), amountIn); - - // fetch the real pool and quote - address pool = HYPERSWAP_FACTORY.getPool( - address(USDT0), - address(WHYPE), - 3000 - ); - - // Create the params struct for quoting - IHyperswapV3QuoterV2.QuoteExactInputSingleParams - memory params = IHyperswapV3QuoterV2.QuoteExactInputSingleParams({ - tokenIn: address(USDT0), - tokenOut: address(WHYPE), - amountIn: amountIn - 1, // Subtract 1 to match slot undrain protection - fee: 3000, - sqrtPriceLimitX96: 0 - }); - - // Get the quote using the struct - (uint256 quoted, , , ) = HYPERSWAP_QUOTER.quoteExactInputSingle( - params - ); - - // Build route using our helper function - bytes memory route = _buildHyperswapV3Route( - HyperswapV3Params({ - commandCode: CommandType.ProcessMyERC20, - tokenIn: address(USDT0), - recipient: USER_SENDER, - pool: pool, - zeroForOne: true // USDT0 < WHYPE - }) - ); - - // expect the Route event - vm.expectEmit(true, true, true, true); - emit Route( - USER_SENDER, - USER_SENDER, - address(USDT0), - address(WHYPE), - amountIn - 1, // Account for slot undrain protection - quoted, - quoted - ); - - // execute - vm.prank(USER_SENDER); - liFiDEXAggregator.processRoute( - address(USDT0), - amountIn - 1, // Account for slot undrain protection - address(WHYPE), - quoted, - USER_SENDER, - route - ); - } - - function test_CanSwap_MultiHop() public override { - // SKIPPED: HyperswapV3 multi-hop unsupported due to AS requirement. - // HyperswapV3 does not support a "one-pool" second hop today, because - // the aggregator (ProcessOnePool) always passes amountSpecified = 0 into - // the pool.swap call. HyperswapV3's swap() immediately reverts on - // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools - // in a single processRoute invocation. - } - - function _buildHyperswapV3Route( - HyperswapV3Params memory params - ) internal pure returns (bytes memory route) { - route = abi.encodePacked( - uint8(params.commandCode), - params.tokenIn, - uint8(1), // 1 pool - FULL_SHARE, // 65535 - 100% share - uint8(PoolType.UniV3), // POOL_TYPE_UNIV3 = 1 - params.pool, - uint8(params.zeroForOne ? 0 : 1), // Convert bool to uint8: 0 for true, 1 for false - params.recipient - ); - - return route; - } -} - -// ----------------------------------------------------------------------------- -// LaminarV3 on HyperEVM -// ----------------------------------------------------------------------------- -contract LiFiDexAggregatorLaminarV3Test is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - IERC20 internal constant WHYPE = - IERC20(0x5555555555555555555555555555555555555555); - IERC20 internal constant LHYPE = - IERC20(0x5748ae796AE46A4F1348a1693de4b50560485562); - - address internal constant WHYPE_LHYPE_POOL = - 0xdAA8a66380fb35b35CB7bc1dBC1925AbfdD0ae45; - - function setUp() public override { - setupHyperEVM(); - } - - function test_CanSwap() public override { - uint256 amountIn = 1_000 * 1e18; - - // Fund the user with WHYPE - deal(address(WHYPE), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - WHYPE.approve(address(liFiDEXAggregator), amountIn); - - // Build a single-pool UniV3 route - bool zeroForOne = address(WHYPE) > address(LHYPE); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(WHYPE), - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.UniV3), - WHYPE_LHYPE_POOL, - uint8(zeroForOne ? 0 : 1), - address(USER_SENDER) - ); - - // Record balances - uint256 inBefore = WHYPE.balanceOf(USER_SENDER); - uint256 outBefore = LHYPE.balanceOf(USER_SENDER); - - // Execute swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(WHYPE), - amountIn, - address(LHYPE), - 0, - USER_SENDER, - route - ); - - // Verify - uint256 inAfter = WHYPE.balanceOf(USER_SENDER); - uint256 outAfter = LHYPE.balanceOf(USER_SENDER); - assertEq(inBefore - inAfter, amountIn, "WHYPE spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive LHYPE"); - - vm.stopPrank(); - } - - function test_CanSwap_FromDexAggregator() public override { - uint256 amountIn = 1_000 * 1e18; - - // fund the aggregator directly - deal(address(WHYPE), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - - bool zeroForOne = address(WHYPE) > address(LHYPE); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), - address(WHYPE), - uint8(1), - FULL_SHARE, - uint8(PoolType.UniV3), - WHYPE_LHYPE_POOL, - uint8(zeroForOne ? 0 : 1), - address(USER_SENDER) - ); - - uint256 outBefore = LHYPE.balanceOf(USER_SENDER); - - // Withdraw 1 wei to avoid slot-undrain protection - liFiDEXAggregator.processRoute( - address(WHYPE), - amountIn - 1, - address(LHYPE), - 0, - USER_SENDER, - route - ); - - uint256 outAfter = LHYPE.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive LHYPE"); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - // SKIPPED: Laminar V3 multi-hop unsupported due to AS requirement. - // Laminar V3 does not support a "one-pool" second hop today, because - // the aggregator (ProcessOnePool) always passes amountSpecified = 0 into - // the pool.swap call. Laminar V3's swap() immediately reverts on - // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools - // in a single processRoute invocation. - } -} - -contract LiFiDexAggregatorXSwapV3Test is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - address internal constant USDC_E_WXDC_POOL = - 0x81B4afF811E94fb084A0d3B3ca456D09AeC14EB0; - - /// @dev our two tokens: USDC.e and wrapped XDC - IERC20 internal constant USDC_E = - IERC20(0x2A8E898b6242355c290E1f4Fc966b8788729A4D4); - IERC20 internal constant WXDC = - IERC20(0x951857744785E80e2De051c32EE7b25f9c458C42); - - function setUp() public override { - customRpcUrlForForking = "ETH_NODE_URI_XDC"; - customBlockNumberForForking = 89279495; - fork(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - function test_CanSwap() public override { - uint256 amountIn = 1_000 * 1e6; - deal(address(USDC_E), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - USDC_E.approve(address(liFiDEXAggregator), amountIn); - - // Build a one-pool V3 route - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(USDC_E), - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.UniV3), - USDC_E_WXDC_POOL, - uint8(1), // zeroForOne (USDC.e > WXDC) - USER_SENDER - ); - - // Record balances before swap - uint256 inBefore = USDC_E.balanceOf(USER_SENDER); - uint256 outBefore = WXDC.balanceOf(USER_SENDER); - - // Execute swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(USDC_E), - amountIn, - address(WXDC), - 0, - USER_SENDER, - route - ); - - // Verify balances after swap - uint256 inAfter = USDC_E.balanceOf(USER_SENDER); - uint256 outAfter = WXDC.balanceOf(USER_SENDER); - assertEq(inBefore - inAfter, amountIn, "USDC.e spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive WXDC"); - - vm.stopPrank(); - } - - /// @notice single-pool swap: aggregator contract sends USDC.e → user receives WXDC - function test_CanSwap_FromDexAggregator() public override { - uint256 amountIn = 5_000 * 1e6; - - // fund the aggregator - deal(address(USDC_E), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - - // Account for slot-undrain protection - uint256 swapAmount = amountIn - 1; - - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), - address(USDC_E), - uint8(1), - FULL_SHARE, - uint8(PoolType.UniV3), - USDC_E_WXDC_POOL, - uint8(1), // zeroForOne (USDC.e > WXDC) - USER_SENDER - ); - - // Record balances before swap - uint256 outBefore = WXDC.balanceOf(USER_SENDER); - - liFiDEXAggregator.processRoute( - address(USDC_E), - swapAmount, - address(WXDC), - 0, - USER_SENDER, - route - ); - - // Verify balances after swap - uint256 outAfter = WXDC.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive WXDC"); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - // SKIPPED: XSwap V3 multi-hop unsupported due to AS requirement. - // XSwap V3 does not support a "one-pool" second hop today, because - // the aggregator (ProcessOnePool) always passes amountSpecified = 0 into - // the pool.swap call. XSwap V3's swap() immediately reverts on - // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools - // in a single processRoute invocation. - } -} - -// ----------------------------------------------------------------------------- -// RabbitSwap on Viction -// ----------------------------------------------------------------------------- -contract LiFiDexAggregatorRabbitSwapTest is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - // Constants for RabbitSwap on Viction - IERC20 internal constant SOROS = - IERC20(0xB786D9c8120D311b948cF1e5Aa48D8fBacf477E2); - IERC20 internal constant C98 = - IERC20(0x0Fd0288AAAE91eaF935e2eC14b23486f86516c8C); - address internal constant SOROS_C98_POOL = - 0xF10eFaE2DdAC396c4ef3c52009dB429A120d0C0D; - - function setUp() public override { - // setup for Viction network - customRpcUrlForForking = "ETH_NODE_URI_VICTION"; - customBlockNumberForForking = 94490946; - fork(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - function test_CanSwap() public override { - uint256 amountIn = 1_000 * 1e18; - - // fund the user with SOROS - deal(address(SOROS), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - SOROS.approve(address(liFiDEXAggregator), amountIn); - - // build a single-pool UniV3-style route - bool zeroForOne = address(SOROS) > address(C98); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(SOROS), - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.UniV3), // RabbitSwap uses UniV3 pool type - SOROS_C98_POOL, - uint8(zeroForOne ? 0 : 1), - address(USER_SENDER) - ); - - // record balances before swap - uint256 inBefore = SOROS.balanceOf(USER_SENDER); - uint256 outBefore = C98.balanceOf(USER_SENDER); - - // execute swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(SOROS), - amountIn, - address(C98), - 0, - USER_SENDER, - route - ); - - // verify balances after swap - uint256 inAfter = SOROS.balanceOf(USER_SENDER); - uint256 outAfter = C98.balanceOf(USER_SENDER); - assertEq(inBefore - inAfter, amountIn, "SOROS spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive C98"); - - vm.stopPrank(); - } - - function test_CanSwap_FromDexAggregator() public override { - uint256 amountIn = 1_000 * 1e18; - - // fund the aggregator directly - deal(address(SOROS), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - - bool zeroForOne = address(SOROS) > address(C98); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), - address(SOROS), - uint8(1), - FULL_SHARE, - uint8(PoolType.UniV3), - SOROS_C98_POOL, - uint8(zeroForOne ? 0 : 1), - address(USER_SENDER) - ); - - uint256 outBefore = C98.balanceOf(USER_SENDER); - - // withdraw 1 wei less to avoid slot-undrain protection - liFiDEXAggregator.processRoute( - address(SOROS), - amountIn - 1, - address(C98), - 0, - USER_SENDER, - route - ); - - uint256 outAfter = C98.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive C98"); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - // SKIPPED: RabbitSwap multi-hop unsupported due to AS requirement. - // RabbitSwap (being a UniV3 fork) does not support a "one-pool" second hop today, - // because the aggregator (ProcessOnePool) always passes amountSpecified = 0 into - // the pool.swap call. UniV3-style pools immediately revert on - // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools - // in a single processRoute invocation. - } - - function testRevert_RabbitSwapInvalidPool() public { - uint256 amountIn = 1_000 * 1e18; - deal(address(SOROS), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - SOROS.approve(address(liFiDEXAggregator), amountIn); - - // build route with invalid pool address - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(SOROS), - uint8(1), - FULL_SHARE, - uint8(PoolType.UniV3), - address(0), // invalid pool address - uint8(0), - USER_SENDER - ); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(SOROS), - amountIn, - address(C98), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - } - - function testRevert_RabbitSwapInvalidRecipient() public { - uint256 amountIn = 1_000 * 1e18; - deal(address(SOROS), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - SOROS.approve(address(liFiDEXAggregator), amountIn); - - // build route with invalid recipient - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(SOROS), - uint8(1), - FULL_SHARE, - uint8(PoolType.UniV3), - SOROS_C98_POOL, - uint8(0), - address(0) // invalid recipient - ); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(SOROS), - amountIn, - address(C98), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - } -} - -// ---------------------------------------------- -// EnosysDexV3 on Flare -// ---------------------------------------------- -contract LiFiDexAggregatorEnosysDexV3Test is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - /// @dev HLN token on Flare - IERC20 internal constant HLN = - IERC20(0x140D8d3649Ec605CF69018C627fB44cCC76eC89f); - - /// @dev USDT0 token on Flare - IERC20 internal constant USDT0 = - IERC20(0xe7cd86e13AC4309349F30B3435a9d337750fC82D); - - /// @dev The single EnosysDexV3 pool for HLN–USDT0 - address internal constant ENOSYS_V3_POOL = - 0xA7C9E7343bD8f1eb7000F25dE5aeb52c6B78B1b7; - - /// @notice Set up a fork of Flare at block 42652369 and initialize the aggregator - function setUp() public override { - customRpcUrlForForking = "ETH_NODE_URI_FLARE"; - customBlockNumberForForking = 42652369; - fork(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - /// @notice Single‐pool swap: USER sends HLN → receives USDT0 - function test_CanSwap() public override { - // Mint 1 000 HLN to USER_SENDER - uint256 amountIn = 1_000 * 1e18; - deal(address(HLN), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - HLN.approve(address(liFiDEXAggregator), amountIn); - - bool zeroForOne = address(HLN) > address(USDT0); - - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(HLN), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.UniV3), // V3‐style pool - ENOSYS_V3_POOL, // pool address - uint8(zeroForOne ? 0 : 1), // 0 = token0→token1, 1 = token1→token0 - address(USER_SENDER) // recipient - ); - - // Record balances before swap - uint256 inBefore = HLN.balanceOf(USER_SENDER); - uint256 outBefore = USDT0.balanceOf(USER_SENDER); - - // Execute the swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(HLN), - amountIn, - address(USDT0), - 0, - USER_SENDER, - route - ); - - // Verify that HLN was spent and some USDT0 was received - uint256 inAfter = HLN.balanceOf(USER_SENDER); - uint256 outAfter = USDT0.balanceOf(USER_SENDER); - - assertEq(inBefore - inAfter, amountIn, "HLN spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive USDT0"); - - vm.stopPrank(); - } - - /// @notice Single‐pool swap: aggregator holds HLN → user receives USDT0 - function test_CanSwap_FromDexAggregator() public override { - // Fund the aggregator with 1 000 HLN - uint256 amountIn = 1_000 * 1e18; - deal(address(HLN), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - bool zeroForOne = address(HLN) > address(USDT0); - - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), // aggregator's funds - address(HLN), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.UniV3), // V3‐style pool - ENOSYS_V3_POOL, // pool address - uint8(zeroForOne ? 0 : 1), // 0 = token0→token1 - address(USER_SENDER) // recipient - ); - - // Subtract 1 to protect against slot‐undrain - uint256 swapAmount = amountIn - 1; - uint256 outBefore = USDT0.balanceOf(USER_SENDER); - - liFiDEXAggregator.processRoute( - address(HLN), - swapAmount, - address(USDT0), - 0, - USER_SENDER, - route - ); - - // Verify that some USDT0 was received - uint256 outAfter = USDT0.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive USDT0"); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - // SKIPPED: EnosysDexV3 multi-hop unsupported due to AS requirement. - // EnosysDexV3 (being a UniV3 fork) does not support a "one-pool" second hop today, - // because the aggregator (ProcessOnePool) always passes amountSpecified = 0 into - // the pool.swap call. UniV3-style pools immediately revert on - // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools - // in a single processRoute invocation. - } -} - -// ---------------------------------------------- -// SyncSwapV2 on Linea -// ---------------------------------------------- -contract LiFiDexAggregatorSyncSwapV2Test is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - IERC20 internal constant USDC = - IERC20(0x176211869cA2b568f2A7D4EE941E073a821EE1ff); - IERC20 internal constant WETH = - IERC20(0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f); - address internal constant USDC_WETH_POOL_V1 = - address(0x5Ec5b1E9b1Bd5198343ABB6E55Fb695d2F7Bb308); - address internal constant SYNC_SWAP_VAULT = - address(0x7160570BB153Edd0Ea1775EC2b2Ac9b65F1aB61B); - - address internal constant USDC_WETH_POOL_V2 = - address(0xDDed227D71A096c6B5D87807C1B5C456771aAA94); - - IERC20 internal constant USDT = - IERC20(0xA219439258ca9da29E9Cc4cE5596924745e12B93); - address internal constant USDC_USDT_POOL_V1 = - address(0x258d5f860B11ec73Ee200eB14f1b60A3B7A536a2); - - /// @notice Set up a fork of Linea at block 20077881 and initialize the aggregator - function setUp() public override { - customRpcUrlForForking = "ETH_NODE_URI_LINEA"; - customBlockNumberForForking = 20077881; - fork(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - /// @notice Single‐pool swap: USER sends WETH → receives USDC - function test_CanSwap() public override { - // Transfer 1 000 WETH from whale to USER_SENDER - uint256 amountIn = 1_000 * 1e18; - deal(address(WETH), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - WETH.approve(address(liFiDEXAggregator), amountIn); - - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V1, // pool address - address(USER_SENDER), // recipient - uint8(2), // withdrawMode - uint8(1), // isV1Pool - address(SYNC_SWAP_VAULT) // vault - ); - - // Record balances before swap - uint256 inBefore = WETH.balanceOf(USER_SENDER); - uint256 outBefore = USDC.balanceOf(USER_SENDER); - - // Execute the swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(WETH), - amountIn, - address(USDC), - 0, - USER_SENDER, - route - ); - - // Verify that WETH was spent and some USDC_C was received - uint256 inAfter = WETH.balanceOf(USER_SENDER); - uint256 outAfter = USDC.balanceOf(USER_SENDER); - - assertEq(inBefore - inAfter, amountIn, "WETH spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive USDC"); - - vm.stopPrank(); - } - - function test_CanSwap_PoolV2() public { - // Transfer 1 000 WETH from whale to USER_SENDER - uint256 amountIn = 1_000 * 1e18; - deal(address(WETH), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - WETH.approve(address(liFiDEXAggregator), amountIn); - - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V2, // pool address - address(USER_SENDER), // recipient - uint8(2), // withdrawMode - uint8(0) // isV1Pool - ); - - // Record balances before swap - uint256 inBefore = WETH.balanceOf(USER_SENDER); - uint256 outBefore = USDC.balanceOf(USER_SENDER); - - // Execute the swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(WETH), - amountIn, - address(USDC), - 0, - USER_SENDER, - route - ); - - // Verify that WETH was spent and some USDC_C was received - uint256 inAfter = WETH.balanceOf(USER_SENDER); - uint256 outAfter = USDC.balanceOf(USER_SENDER); - - assertEq(inBefore - inAfter, amountIn, "WETH spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive USDC"); - - vm.stopPrank(); - } - - function test_CanSwap_FromDexAggregator() public override { - // Fund the aggregator with 1 000 WETH - uint256 amountIn = 1_000 * 1e18; - deal(address(WETH), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), // aggregator's funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V1, // pool address - address(USER_SENDER), // recipient - uint8(2), // withdrawMode - uint8(1), // isV1Pool - address(SYNC_SWAP_VAULT) // vault - ); - - // Subtract 1 to protect against slot‐undrain - uint256 swapAmount = amountIn - 1; - uint256 outBefore = USDC.balanceOf(USER_SENDER); - - liFiDEXAggregator.processRoute( - address(WETH), - swapAmount, - address(USDC), - 0, - USER_SENDER, - route - ); - - // Verify that some USDC was received - uint256 outAfter = USDC.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive USDC"); - - vm.stopPrank(); - } - - function test_CanSwap_FromDexAggregator_PoolV2() public { - // Fund the aggregator with 1 000 WETH - uint256 amountIn = 1_000 * 1e18; - deal(address(WETH), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), // aggregator's funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V2, // pool address - address(USER_SENDER), // recipient - uint8(2) // withdrawMode - ); - - // Subtract 1 to protect against slot‐undrain - uint256 swapAmount = amountIn - 1; - uint256 outBefore = USDC.balanceOf(USER_SENDER); - - liFiDEXAggregator.processRoute( - address(WETH), - swapAmount, - address(USDC), - 0, - USER_SENDER, - route - ); - - // Verify that some USDC was received - uint256 outAfter = USDC.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive USDC"); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - uint256 amountIn = 1_000e18; - deal(address(WETH), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - WETH.approve(address(liFiDEXAggregator), amountIn); - - uint256 initialBalanceIn = WETH.balanceOf(USER_SENDER); - uint256 initialBalanceOut = USDT.balanceOf(USER_SENDER); - - // - // 1) PROCESS_USER_ERC20: WETH → USDC (SyncSwap V1 → withdrawMode=2 → vault that still holds USDC) - // - bytes memory hop1 = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(WETH), - uint8(1), // one pool - FULL_SHARE, // 100% of the WETH - uint8(PoolType.SyncSwapV2), - USDC_WETH_POOL_V1, // the V1 pool - SYNC_SWAP_VAULT, // “to” = the vault address - uint8(2), // withdrawMode = 2 - uint8(1), // isV1Pool = true - address(SYNC_SWAP_VAULT) // vault - ); - - // - // 2) PROCESS_ONE_POOL: now swap that USDC → USDT via SyncSwap pool V1 - // - bytes memory hop2 = abi.encodePacked( - uint8(CommandType.ProcessOnePool), - address(USDC), - uint8(PoolType.SyncSwapV2), - USDC_USDT_POOL_V1, // V1 USDC⟶USDT pool - address(USER_SENDER), // send the USDT home - uint8(2), // withdrawMode = 2 - uint8(1), // isV1Pool = true - SYNC_SWAP_VAULT // vault - ); - - bytes memory route = bytes.concat(hop1, hop2); - - uint256 amountOut = liFiDEXAggregator.processRoute( - address(WETH), - amountIn, - address(USDT), - 0, - USER_SENDER, - route - ); - - uint256 afterBalanceIn = WETH.balanceOf(USER_SENDER); - uint256 afterBalanceOut = USDT.balanceOf(USER_SENDER); - - assertEq( - initialBalanceIn - afterBalanceIn, - amountIn, - "WETH spent mismatch" - ); - assertEq( - amountOut, - afterBalanceOut - initialBalanceOut, - "USDT amountOut mismatch" - ); - vm.stopPrank(); - } - - function testRevert_V1PoolMissingVaultAddress() public { - // Transfer 1 000 WETH from whale to USER_SENDER - uint256 amountIn = 1_000 * 1e18; - deal(address(WETH), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - WETH.approve(address(liFiDEXAggregator), amountIn); - - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V1, // pool address - address(USER_SENDER), // recipient - uint8(2), // withdrawMode - uint8(1), // isV1Pool - address(0) // vault (invalid address) - ); - - // Expect revert with InvalidCallData - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(WETH), - amountIn, - address(USDC), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - } - - function testRevert_InvalidPoolOrRecipient() public { - // Transfer 1 000 WETH from whale to USER_SENDER - uint256 amountIn = 1_000 * 1e18; - deal(address(WETH), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - WETH.approve(address(liFiDEXAggregator), amountIn); - - bytes memory routeWithInvalidPool = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - address(0), // pool address (invalid address) - address(USER_SENDER), // recipient - uint8(2), // withdrawMode - uint8(1), // isV1Pool - address(SYNC_SWAP_VAULT) // vault - ); - - // Expect revert with InvalidCallData - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(WETH), - amountIn, - address(USDC), - 0, - USER_SENDER, - routeWithInvalidPool - ); - - bytes memory routeWithInvalidRecipient = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V1, // pool address - address(0), // recipient (invalid address) - uint8(2), // withdrawMode - uint8(1), // isV1Pool - address(SYNC_SWAP_VAULT) // vault - ); - - // Expect revert with InvalidCallData - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(WETH), - amountIn, - address(USDC), - 0, - USER_SENDER, - routeWithInvalidRecipient - ); - - vm.stopPrank(); - } - - function testRevert_InvalidWithdrawMode() public { - vm.startPrank(USER_SENDER); - - bytes memory routeWithInvalidWithdrawMode = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), // user funds - address(WETH), // tokenIn - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.SyncSwapV2), // SyncSwapV2 - USDC_WETH_POOL_V1, // pool address (invalid address) - address(USER_SENDER), // recipient - uint8(3), // withdrawMode (invalid) - uint8(1), // isV1Pool - address(SYNC_SWAP_VAULT) // vault - ); - - // Expect revert with InvalidCallData because withdrawMode is invalid - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(WETH), - 1, - address(USDC), - 0, - USER_SENDER, - routeWithInvalidWithdrawMode - ); - - vm.stopPrank(); - } -} - -// ----------------------------------------------------------------------------- -// KatanaV3 on Ronin -// ----------------------------------------------------------------------------- -contract LiFiDexAggregatorKatanaV3Test is LiFiDexAggregatorTest { - using SafeERC20 for IERC20; - - // Constants for KatanaV3 on Ronin - IERC20 internal constant USDC = - IERC20(0x0B7007c13325C48911F73A2daD5FA5dCBf808aDc); - IERC20 internal constant WRAPPED_RON = - IERC20(0xe514d9DEB7966c8BE0ca922de8a064264eA6bcd4); - address internal constant USDC_WRAPPED_RON_POOL = - 0x392d372F2A51610E9AC5b741379D5631Ca9A1c7f; - - function setUp() public override { - // setup for Viction network - customRpcUrlForForking = "ETH_NODE_URI_RONIN"; - customBlockNumberForForking = 47105304; - fork(); - - _initializeDexAggregator(USER_DIAMOND_OWNER); - } - - function test_CanSwap() public override { - uint256 amountIn = 1 * 1e6; - - // fund the user with USDC - deal(address(USDC), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - USDC.approve(address(liFiDEXAggregator), amountIn); - - // build a single-pool UniV3-style route - bool zeroForOne = address(USDC) > address(WRAPPED_RON); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(USDC), - uint8(1), // one pool - FULL_SHARE, // 100% - uint8(PoolType.KatanaV3), // KatanaV3 - USDC_WRAPPED_RON_POOL, - uint8(zeroForOne ? 0 : 1), - address(USER_SENDER) - ); - - // record balances before swap - uint256 inBefore = USDC.balanceOf(USER_SENDER); - uint256 outBefore = WRAPPED_RON.balanceOf(USER_SENDER); - - // execute swap (minOut = 0 for test) - liFiDEXAggregator.processRoute( - address(USDC), - amountIn, - address(WRAPPED_RON), - 0, - USER_SENDER, - route - ); - - // verify balances after swap - uint256 inAfter = USDC.balanceOf(USER_SENDER); - uint256 outAfter = WRAPPED_RON.balanceOf(USER_SENDER); - assertEq(inBefore - inAfter, amountIn, "USDC spent mismatch"); - assertGt(outAfter - outBefore, 0, "Should receive WRAPPED_RON"); - - vm.stopPrank(); - } - - function test_CanSwap_FromDexAggregator() public override { - uint256 amountIn = 1 * 1e6; - - // fund the aggregator directly - deal(address(USDC), address(liFiDEXAggregator), amountIn); - - vm.startPrank(USER_SENDER); - - bool zeroForOne = address(USDC) > address(WRAPPED_RON); - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessMyERC20), - address(USDC), - uint8(1), - FULL_SHARE, - uint8(PoolType.KatanaV3), - USDC_WRAPPED_RON_POOL, - uint8(zeroForOne ? 0 : 1), - address(USER_SENDER) - ); - - uint256 outBefore = WRAPPED_RON.balanceOf(USER_SENDER); - - // withdraw 1 wei less to avoid slot-undrain protection - liFiDEXAggregator.processRoute( - address(USDC), - amountIn - 1, - address(WRAPPED_RON), - 0, - USER_SENDER, - route - ); - - uint256 outAfter = WRAPPED_RON.balanceOf(USER_SENDER); - assertGt(outAfter - outBefore, 0, "Should receive WRAPPED_RON"); - - vm.stopPrank(); - } - - function test_CanSwap_MultiHop() public override { - // SKIPPED: KatanaV3 multi-hop unsupported due to AS requirement. - // KatanaV3 (being a similar implementation to UniV3) does not support a "one-pool" second hop today, - // because the aggregator (ProcessOnePool) always passes amountSpecified = 0 into - // the pool.swap call. UniV3-style pools immediately revert on - // require(amountSpecified != 0, 'AS'), so you can't chain two V3 pools in a single processRoute invocation. - } - - function testRevert_KatanaV3InvalidPool() public { - uint256 amountIn = 1_000 * 1e6; - deal(address(USDC), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - USDC.approve(address(liFiDEXAggregator), amountIn); - - // build route with invalid pool address - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(USDC), - uint8(1), - FULL_SHARE, - uint8(PoolType.KatanaV3), - address(0), // invalid pool address - uint8(0), - USER_SENDER - ); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(USDC), - amountIn, - address(WRAPPED_RON), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - } - - function testRevert_KatanaV3InvalidRecipient() public { - uint256 amountIn = 1_000 * 1e6; - deal(address(USDC), USER_SENDER, amountIn); - - vm.startPrank(USER_SENDER); - USDC.approve(address(liFiDEXAggregator), amountIn); - - // build route with invalid recipient - bytes memory route = abi.encodePacked( - uint8(CommandType.ProcessUserERC20), - address(USDC), - uint8(1), - FULL_SHARE, - uint8(PoolType.KatanaV3), - USDC_WRAPPED_RON_POOL, - uint8(0), - address(0) // invalid recipient - ); - - vm.expectRevert(InvalidCallData.selector); - liFiDEXAggregator.processRoute( - address(USDC), - amountIn, - address(WRAPPED_RON), - 0, - USER_SENDER, - route - ); - - vm.stopPrank(); - } -} diff --git a/test/solidity/Periphery/LidoWrapper/LidoWrapper.t.sol b/test/solidity/Periphery/LidoWrapper/LidoWrapper.t.sol index 989b1db33..dce9dc86f 100644 --- a/test/solidity/Periphery/LidoWrapper/LidoWrapper.t.sol +++ b/test/solidity/Periphery/LidoWrapper/LidoWrapper.t.sol @@ -43,7 +43,7 @@ contract LidoWrapperTest is TestBase { error ContractNotYetReadyForMainnet(); - function setUp() public { + function setUp() public override { vm.label(ST_ETH_ADDRESS_OPTIMISM, "stETH"); vm.label(WST_ETH_ADDRESS_OPTIMISM, "wstETH"); @@ -95,7 +95,7 @@ contract LidoWrapperTest is TestBase { functionSelectors[3] = relayFacet.getMappedChainId.selector; functionSelectors[4] = relayFacet.setConsumedId.selector; - addFacet(diamond, address(relayFacet), functionSelectors); + addFacet(address(diamond), address(relayFacet), functionSelectors); // slither-disable-next-line reentrancy-no-eth relayFacet = TestRelayFacet(address(diamond)); @@ -502,7 +502,11 @@ contract LidoWrapperTest is TestBase { .selector; // add facet to diamond and store diamond with facet interface in variable - addFacet(diamond, address(genericSwapFacetV3), functionSelectors); + addFacet( + address(diamond), + address(genericSwapFacetV3), + functionSelectors + ); genericSwapFacetV3 = GenericSwapFacetV3(address(diamond)); } } diff --git a/test/solidity/Periphery/LidoWrapper/LidoWrapperSON.t.sol b/test/solidity/Periphery/LidoWrapper/LidoWrapperSON.t.sol index d5d325ecb..8d33e3357 100644 --- a/test/solidity/Periphery/LidoWrapper/LidoWrapperSON.t.sol +++ b/test/solidity/Periphery/LidoWrapper/LidoWrapperSON.t.sol @@ -14,7 +14,8 @@ contract LidoWrapperTestSON is TestBase { address private constant ST_ETH_WHALE = 0xB67FB1422ACa6F017BFdF1c40b372dA9eEdD03BF; - function setUp() public { + function setUp() public override { + super.setUp(); vm.label(ST_ETH_ADDRESS, "stETH"); vm.label(WST_ETH_ADDRESS, "wstETH"); diff --git a/test/solidity/Periphery/LidoWrapper/LidoWrapperUNI.t.sol b/test/solidity/Periphery/LidoWrapper/LidoWrapperUNI.t.sol index e8738c862..f66c7b83d 100644 --- a/test/solidity/Periphery/LidoWrapper/LidoWrapperUNI.t.sol +++ b/test/solidity/Periphery/LidoWrapper/LidoWrapperUNI.t.sol @@ -16,7 +16,8 @@ contract LidoWrapperTestUNI is TestBase { uint256 private whaleBalance; - function setUp() public { + function setUp() public override { + super.setUp(); vm.label(ST_ETH_ADDRESS, "stETH"); vm.label(WST_ETH_ADDRESS, "wstETH"); diff --git a/test/solidity/Periphery/Patcher.t.sol b/test/solidity/Periphery/Patcher.t.sol index 4865a772e..fe0f54bd8 100644 --- a/test/solidity/Periphery/Patcher.t.sol +++ b/test/solidity/Periphery/Patcher.t.sol @@ -225,7 +225,7 @@ contract PatcherTest is TestBase { uint256 internal privateKey = 0x1234567890; address internal relaySolver; - function setUp() public { + function setUp() public override { initTestBase(); patcher = new Patcher(); valueSource = new MockValueSource(); diff --git a/test/solidity/Periphery/Permit2Proxy.t.sol b/test/solidity/Periphery/Permit2Proxy.t.sol index 34710c55d..059939f8d 100644 --- a/test/solidity/Periphery/Permit2Proxy.t.sol +++ b/test/solidity/Periphery/Permit2Proxy.t.sol @@ -117,7 +117,7 @@ contract Permit2ProxyTest is TestBase { error DiamondAddressNotWhitelisted(); error CallToDiamondFailed(bytes); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 20261175; initTestBase(); @@ -293,7 +293,7 @@ contract Permit2ProxyTest is TestBase { ADDRESS_USDC, defaultUSDCAmount, testdata.deadline, - testdata.v + 1, // invalid v value + 30, // invalid v value (valid values are 27, 28) testdata.r, testdata.s, testdata.diamondCalldata diff --git a/test/solidity/Periphery/ReceiverChainflip.t.sol b/test/solidity/Periphery/ReceiverChainflip.t.sol index 72825fc30..6f6038624 100644 --- a/test/solidity/Periphery/ReceiverChainflip.t.sol +++ b/test/solidity/Periphery/ReceiverChainflip.t.sol @@ -27,7 +27,7 @@ contract ReceiverChainflipTest is TestBase { event ExecutorSet(address indexed executor); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 18277082; initTestBase(); diff --git a/test/solidity/Periphery/ReceiverStargateV2.t.sol b/test/solidity/Periphery/ReceiverStargateV2.t.sol index 23f24f3a1..cbbf5daa0 100644 --- a/test/solidity/Periphery/ReceiverStargateV2.t.sol +++ b/test/solidity/Periphery/ReceiverStargateV2.t.sol @@ -38,7 +38,7 @@ contract ReceiverStargateV2Test is TestBase { event ExecutorSet(address indexed executor); event RecoverGasSet(uint256 indexed recoverGas); - function setUp() public { + function setUp() public override { customBlockNumberForForking = 20024274; initTestBase(); diff --git a/test/solidity/utils/BaseDiamondTest.sol b/test/solidity/utils/BaseDiamondTest.sol new file mode 100644 index 000000000..befec8d51 --- /dev/null +++ b/test/solidity/utils/BaseDiamondTest.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; +import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; +import { EmergencyPauseFacet } from "lifi/Facets/EmergencyPauseFacet.sol"; +import { PeripheryRegistryFacet } from "lifi/Facets/PeripheryRegistryFacet.sol"; +import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; +import { DiamondTestHelpers } from "./DiamondTestHelpers.sol"; + +/// @notice Base contract with common diamond test functions to reduce code duplication +/// @dev Provides standard test patterns that work with any diamond implementation +abstract contract BaseDiamondTest is DiamondTestHelpers { + // Main diamond instance - accessible to all inheriting contracts including TestBase + LiFiDiamond internal diamond; + + // Common facet instances + DiamondCutFacet internal diamondCutFacet; + OwnershipFacet internal ownershipFacet; + DiamondLoupeFacet internal diamondLoupeFacet; + PeripheryRegistryFacet internal peripheryFacet; + EmergencyPauseFacet internal emergencyPauseFacet; + + // Events + event DiamondCut( + LibDiamond.FacetCut[] _diamondCut, + address _init, + bytes _calldata + ); + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + // Errors + error FunctionDoesNotExist(); + error ShouldNotReachThisCode(); + error InvalidDiamondSetup(); + error ExternalCallFailed(); + + function setUp() public virtual { + // Create the main LiFiDiamond that TestBase and other facet tests expect + diamond = createDiamond(USER_DIAMOND_OWNER, USER_PAUSER); + } + + /// @notice Creates a fully configured diamond with all standard facets + /// @param _diamondOwner Owner address for the diamond + /// @param _pauserWallet Pauser wallet address for emergency pause facet + /// @return The created LiFiDiamond instance + function createDiamond( + address _diamondOwner, + address _pauserWallet + ) internal returns (LiFiDiamond) { + vm.startPrank(_diamondOwner); + + // Recreate facets with the specified pauser + diamondCutFacet = new DiamondCutFacet(); + diamondLoupeFacet = new DiamondLoupeFacet(); + ownershipFacet = new OwnershipFacet(); + peripheryFacet = new PeripheryRegistryFacet(); + emergencyPauseFacet = new EmergencyPauseFacet(_pauserWallet); + + // Create new diamond + diamond = new LiFiDiamond(_diamondOwner, address(diamondCutFacet)); + + // Add Diamond Loupe + _addDiamondLoupeSelectors(address(diamondLoupeFacet)); + + // Add Ownership + _addOwnershipSelectors(address(ownershipFacet)); + + // Add PeripheryRegistry + bytes4[] memory functionSelectors = new bytes4[](2); + functionSelectors[0] = PeripheryRegistryFacet + .registerPeripheryContract + .selector; + functionSelectors[1] = PeripheryRegistryFacet + .getPeripheryContract + .selector; + cut.push( + LibDiamond.FacetCut({ + facetAddress: address(peripheryFacet), + action: LibDiamond.FacetCutAction.Add, + functionSelectors: functionSelectors + }) + ); + + // Add EmergencyPause + functionSelectors = new bytes4[](3); + functionSelectors[0] = emergencyPauseFacet.removeFacet.selector; + functionSelectors[1] = emergencyPauseFacet.pauseDiamond.selector; + functionSelectors[2] = emergencyPauseFacet.unpauseDiamond.selector; + cut.push( + LibDiamond.FacetCut({ + facetAddress: address(emergencyPauseFacet), + action: LibDiamond.FacetCutAction.Add, + functionSelectors: functionSelectors + }) + ); + + DiamondCutFacet(address(diamond)).diamondCut(cut, address(0), ""); + delete cut; + vm.stopPrank(); + return diamond; + } + + /// @notice Override this to return the diamond address for testing + function getDiamondAddress() internal view virtual returns (address) { + return address(diamond); + } + + /// @notice Override this to return the diamond owner address + function getDiamondOwner() internal view virtual returns (address) { + return USER_DIAMOND_OWNER; + } + + /// @notice Test that diamond deployment works without errors + function test_DeploysWithoutErrors() public virtual { + assertTrue( + getDiamondAddress() != address(0), + "Diamond should be deployed" + ); + } + + /// @notice Test that diamond forwards calls via delegate call + function test_ForwardsCallsViaDelegateCall() public { + address diamondAddr = getDiamondAddress(); + address owner = getDiamondOwner(); + + vm.startPrank(owner); + + DiamondLoupeFacet diamondLoupe = new DiamondLoupeFacet(); + + // Check if DiamondLoupeFacet is already installed + bool loupeAlreadyInstalled = false; + try DiamondLoupeFacet(diamondAddr).facetAddresses() returns ( + address[] memory + ) { + loupeAlreadyInstalled = true; + } catch { + // Loupe not installed, which is expected for basic diamonds + } + + if (!loupeAlreadyInstalled) { + // prepare function selectors + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = diamondLoupe.facets.selector; + functionSelectors[1] = diamondLoupe + .facetFunctionSelectors + .selector; + functionSelectors[2] = diamondLoupe.facetAddresses.selector; + functionSelectors[3] = diamondLoupe.facetAddress.selector; + + // prepare diamondCut + LibDiamond.FacetCut[] memory cuts = new LibDiamond.FacetCut[](1); + cuts[0] = LibDiamond.FacetCut({ + facetAddress: address(diamondLoupe), + action: LibDiamond.FacetCutAction.Add, + functionSelectors: functionSelectors + }); + + DiamondCutFacet(diamondAddr).diamondCut(cuts, address(0), ""); + } + + // Now the call should succeed + address[] memory facetAddresses = DiamondLoupeFacet(diamondAddr) + .facetAddresses(); + assertTrue( + facetAddresses.length > 0, + "Should have facets after adding DiamondLoupe" + ); + + vm.stopPrank(); + } + + /// @notice Test that diamond reverts on unknown function selectors + function test_RevertsOnUnknownFunctionSelector() public { + address diamondAddr = getDiamondAddress(); + + // Use a completely random selector that definitely doesn't exist + bytes memory callData = hex"deadbeef"; + + vm.expectRevert(FunctionDoesNotExist.selector); + (bool success, ) = diamondAddr.call(callData); + if (!success) { + vm.expectRevert("Diamond: Function does not exist"); + (bool success2, ) = diamondAddr.call(callData); + if (!success2) { + revert ShouldNotReachThisCode(); + } + } + } + + /// @notice Test that diamond can receive ETH + function test_CanReceiveETH() public { + address diamondAddr = getDiamondAddress(); + uint256 balanceBefore = diamondAddr.balance; + (bool success, ) = diamondAddr.call{ value: 1 ether }(""); + if (!success) revert ExternalCallFailed(); + + assertEq(address(diamond).balance, balanceBefore + 1 ether); + } + + /// @notice Test that diamond owner was correctly registered + function test_DiamondOwnerIsCorrectlyRegistered() public { + address diamondAddr = getDiamondAddress(); + address expectedOwner = getDiamondOwner(); + + // Get the actual owner from the diamond + address actualOwner = OwnershipFacet(diamondAddr).owner(); + + assertEq( + actualOwner, + expectedOwner, + "Diamond owner should match expected owner" + ); + } +} diff --git a/test/solidity/utils/DiamondTest.sol b/test/solidity/utils/DiamondTest.sol deleted file mode 100644 index e985f35bb..000000000 --- a/test/solidity/utils/DiamondTest.sol +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.17; - -import { LiFiDiamond } from "lifi/LiFiDiamond.sol"; -import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; -import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; -import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; -import { EmergencyPauseFacet } from "lifi/Facets/EmergencyPauseFacet.sol"; -import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; -import { PeripheryRegistryFacet } from "lifi/Facets/PeripheryRegistryFacet.sol"; -import { Test } from "forge-std/Test.sol"; - -contract DiamondTest is Test { - LibDiamond.FacetCut[] internal cut; - - function createDiamond( - address _diamondOwner, - address _pauserWallet - ) internal returns (LiFiDiamond) { - vm.startPrank(_diamondOwner); - DiamondCutFacet diamondCut = new DiamondCutFacet(); - DiamondLoupeFacet diamondLoupe = new DiamondLoupeFacet(); - OwnershipFacet ownership = new OwnershipFacet(); - PeripheryRegistryFacet periphery = new PeripheryRegistryFacet(); - EmergencyPauseFacet emergencyPause = new EmergencyPauseFacet( - _pauserWallet - ); - LiFiDiamond diamond = new LiFiDiamond( - _diamondOwner, - address(diamondCut) - ); - - bytes4[] memory functionSelectors; - - // Diamond Loupe - - functionSelectors = new bytes4[](5); - functionSelectors[0] = DiamondLoupeFacet - .facetFunctionSelectors - .selector; - functionSelectors[1] = DiamondLoupeFacet.facets.selector; - functionSelectors[2] = DiamondLoupeFacet.facetAddress.selector; - functionSelectors[3] = DiamondLoupeFacet.facetAddresses.selector; - functionSelectors[4] = DiamondLoupeFacet.supportsInterface.selector; - cut.push( - LibDiamond.FacetCut({ - facetAddress: address(diamondLoupe), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: functionSelectors - }) - ); - - // Ownership Facet - - functionSelectors = new bytes4[](4); - functionSelectors[0] = OwnershipFacet.transferOwnership.selector; - functionSelectors[1] = OwnershipFacet.cancelOwnershipTransfer.selector; - functionSelectors[2] = OwnershipFacet - .confirmOwnershipTransfer - .selector; - functionSelectors[3] = OwnershipFacet.owner.selector; - - cut.push( - LibDiamond.FacetCut({ - facetAddress: address(ownership), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: functionSelectors - }) - ); - - // PeripheryRegistryFacet - functionSelectors = new bytes4[](2); - functionSelectors[0] = PeripheryRegistryFacet - .registerPeripheryContract - .selector; - functionSelectors[1] = PeripheryRegistryFacet - .getPeripheryContract - .selector; - - cut.push( - LibDiamond.FacetCut({ - facetAddress: address(periphery), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: functionSelectors - }) - ); - - // EmergencyPauseFacet - functionSelectors = new bytes4[](3); - functionSelectors[0] = emergencyPause.removeFacet.selector; - functionSelectors[1] = emergencyPause.pauseDiamond.selector; - functionSelectors[2] = emergencyPause.unpauseDiamond.selector; - - cut.push( - LibDiamond.FacetCut({ - facetAddress: address(emergencyPause), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: functionSelectors - }) - ); - - DiamondCutFacet(address(diamond)).diamondCut(cut, address(0), ""); - - delete cut; - - vm.stopPrank(); - return diamond; - } - - function addFacet( - LiFiDiamond _diamond, - address _facet, - bytes4[] memory _selectors - ) internal { - _addFacet(_diamond, _facet, _selectors, address(0), ""); - } - - function addFacet( - LiFiDiamond _diamond, - address _facet, - bytes4[] memory _selectors, - address _init, - bytes memory _initCallData - ) internal { - _addFacet(_diamond, _facet, _selectors, _init, _initCallData); - } - - function _addFacet( - LiFiDiamond _diamond, - address _facet, - bytes4[] memory _selectors, - address _init, - bytes memory _initCallData - ) internal { - vm.startPrank(OwnershipFacet(address(_diamond)).owner()); - cut.push( - LibDiamond.FacetCut({ - facetAddress: _facet, - action: LibDiamond.FacetCutAction.Add, - functionSelectors: _selectors - }) - ); - - DiamondCutFacet(address(_diamond)).diamondCut( - cut, - _init, - _initCallData - ); - - delete cut; - vm.stopPrank(); - } -} diff --git a/test/solidity/utils/DiamondTestHelpers.sol b/test/solidity/utils/DiamondTestHelpers.sol new file mode 100644 index 000000000..775f91cf4 --- /dev/null +++ b/test/solidity/utils/DiamondTestHelpers.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import { DiamondCutFacet } from "lifi/Facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "lifi/Facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "lifi/Facets/OwnershipFacet.sol"; +import { LibDiamond } from "lifi/Libraries/LibDiamond.sol"; +import { Test } from "forge-std/Test.sol"; +import { TestBaseRandomConstants } from "./TestBaseRandomConstants.sol"; + +/// @notice Minimal helper to compose a test Diamond and add facets/selectors for test scenarios. +/// @dev Provides overloads to add facets with or without init calldata. +/// This contract is used by higher-level LDA test scaffolding to assemble the test Diamond. +abstract contract DiamondTestHelpers is Test, TestBaseRandomConstants { + LibDiamond.FacetCut[] internal cut; + + /// @notice Adds standard Diamond Loupe selectors to the `cut` buffer. + /// @param _diamondLoupe Address of a deployed `DiamondLoupeFacet`. + /// @dev Call this before invoking diamondCut with the buffered `cut`. + function _addDiamondLoupeSelectors(address _diamondLoupe) internal { + bytes4[] memory functionSelectors = new bytes4[](5); + functionSelectors[0] = DiamondLoupeFacet + .facetFunctionSelectors + .selector; + functionSelectors[1] = DiamondLoupeFacet.facets.selector; + functionSelectors[2] = DiamondLoupeFacet.facetAddress.selector; + functionSelectors[3] = DiamondLoupeFacet.facetAddresses.selector; + functionSelectors[4] = DiamondLoupeFacet.supportsInterface.selector; + + cut.push( + LibDiamond.FacetCut({ + facetAddress: _diamondLoupe, + action: LibDiamond.FacetCutAction.Add, + functionSelectors: functionSelectors + }) + ); + } + + /// @notice Adds standard Ownership selectors to the `cut` buffer. + /// @param _ownership Address of a deployed `OwnershipFacet`. + /// @dev Call this before invoking diamondCut with the buffered `cut`. + function _addOwnershipSelectors(address _ownership) internal { + bytes4[] memory functionSelectors = new bytes4[](4); + functionSelectors[0] = OwnershipFacet.transferOwnership.selector; + functionSelectors[1] = OwnershipFacet.cancelOwnershipTransfer.selector; + functionSelectors[2] = OwnershipFacet + .confirmOwnershipTransfer + .selector; + functionSelectors[3] = OwnershipFacet.owner.selector; + + cut.push( + LibDiamond.FacetCut({ + facetAddress: _ownership, + action: LibDiamond.FacetCutAction.Add, + functionSelectors: functionSelectors + }) + ); + } + + /// @notice Adds a facet and function selectors to the target diamond. + /// @param _diamond Address of the diamond proxy. + /// @param _facet Address of the facet implementation to add. + /// @param _selectors Function selectors to expose in the diamond. + /// @dev Convenience overload with no initializer; see the 5-arg overload for init flows. + function addFacet( + address _diamond, + address _facet, + bytes4[] memory _selectors + ) public virtual { + _addFacet(_diamond, _facet, _selectors, address(0), ""); + } + + /// @notice Adds a facet and function selectors to the target diamond, optionally executing an initializer. + /// @param _diamond Address of the diamond proxy. + /// @param _facet Address of the facet implementation to add. + /// @param _selectors Function selectors to expose in the diamond. + /// @param _init Address of an initializer (can be facet or another contract). + /// @param _initCallData ABI-encoded calldata for the initializer. + /// @dev Owner is impersonated via vm.startPrank for the duration of the diamondCut. + function addFacet( + address _diamond, + address _facet, + bytes4[] memory _selectors, + address _init, + bytes memory _initCallData + ) public virtual { + _addFacet(_diamond, _facet, _selectors, _init, _initCallData); + } + + /// @notice Performs diamondCut with an appended `FacetCut`. + /// @param _diamond Address of the diamond proxy. + /// @param _facet Address of the facet implementation to add. + /// @param _selectors Function selectors to expose in the diamond. + /// @param _init Address of an initializer (address(0) for none). + /// @param _initCallData ABI-encoded calldata for the initializer (empty if none). + /// @dev Example: + /// - Append loupe + ownership cuts first. + /// - Then call `_addFacet(diamond, address(myFacet), selectors, address(0), "")`. + function _addFacet( + address _diamond, + address _facet, + bytes4[] memory _selectors, + address _init, + bytes memory _initCallData + ) internal virtual { + vm.startPrank(OwnershipFacet(_diamond).owner()); + cut.push( + LibDiamond.FacetCut({ + facetAddress: _facet, + action: LibDiamond.FacetCutAction.Add, + functionSelectors: _selectors + }) + ); + + DiamondCutFacet(_diamond).diamondCut(cut, _init, _initCallData); + + delete cut; + vm.stopPrank(); + } +} diff --git a/test/solidity/utils/MockNoCallbackPool.sol b/test/solidity/utils/MockNoCallbackPool.sol new file mode 100644 index 000000000..f3afe1e46 --- /dev/null +++ b/test/solidity/utils/MockNoCallbackPool.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +import { IUniV3StylePool } from "lifi/Interfaces/IUniV3StylePool.sol"; + +/// @title MockNoCallbackPool +/// @author LI.FI (https://li.fi) +/// @notice Mock pool that simulates successful swaps without executing callbacks +/// @dev Used to test callback verification in facets. This mock: +/// 1. Implements UniV3-style interface for base compatibility +/// 2. Returns dummy token addresses for token0/token1 +/// 3. Returns (0,0) for all swap calls without executing callbacks +/// 4. Catches all non-standard swap calls (like Izumi's swapX2Y) via fallback +/// @custom:version 1.0.0 +contract MockNoCallbackPool is IUniV3StylePool { + /// @notice Returns a dummy token0 address + function token0() external pure returns (address) { + return address(1); + } + + /// @notice Returns a dummy token1 address + function token1() external pure returns (address) { + return address(2); + } + + /// @notice UniV3-style swap that doesn't execute callbacks + /// @dev Always returns (0,0) to simulate successful swap without callback + function swap( + address, + bool, + int256, + uint160, + bytes calldata + ) external pure returns (int256, int256) { + // Simulate successful swap without executing callback + return (0, 0); + } + + /// @notice Catch-all for non-standard swap functions (Izumi, Algebra, etc) + /// @dev Returns (0,0) encoded as bytes to match return types of various swap functions + fallback() external { + assembly { + mstore(0x00, 0) + mstore(0x20, 0) + return(0x00, 0x40) // Return (0,0) for any swap function + } + } + + /// @notice Required to receive ETH from swaps if needed + receive() external payable {} +} diff --git a/test/solidity/utils/TestBase.sol b/test/solidity/utils/TestBase.sol index 435a3cfea..ec36fe290 100644 --- a/test/solidity/utils/TestBase.sol +++ b/test/solidity/utils/TestBase.sol @@ -1,16 +1,19 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.17; +import { ERC20 } from "solmate/tokens/ERC20.sol"; import { Test } from "forge-std/Test.sol"; +import { stdJson } from "forge-std/StdJson.sol"; import { DSTest } from "ds-test/test.sol"; import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; -import { UniswapV2Router02 } from "../utils/Interfaces.sol"; -import { DiamondTest, LiFiDiamond } from "../utils/DiamondTest.sol"; -import { ERC20 } from "solmate/tokens/ERC20.sol"; import { FeeCollector } from "lifi/Periphery/FeeCollector.sol"; -import { ReentrancyError, ETHTransferFailed } from "src/Errors/GenericErrors.sol"; -import { stdJson } from "forge-std/StdJson.sol"; +import { ReentrancyError, ETHTransferFailed } from "lifi/Errors/GenericErrors.sol"; +import { LiFiDEXAggregatorDiamondTest } from "../Periphery/LDA/LiFiDEXAggregatorDiamond.t.sol"; +import { UniswapV2Router02 } from "../utils/Interfaces.sol"; +import { TestBaseForksConstants } from "./TestBaseForksConstants.sol"; +import { TestBaseRandomConstants } from "./TestBaseRandomConstants.sol"; +import { TestHelpers } from "./TestHelpers.sol"; import { TestWhitelistManagerBase } from "./TestWhitelistManagerBase.sol"; import { LiFiData } from "src/Helpers/LiFiData.sol"; @@ -82,7 +85,15 @@ contract ReentrancyChecker is DSTest { //common utilities for forge tests // solhint-disable max-states-count -abstract contract TestBase is Test, DiamondTest, ILiFi, LiFiData { +abstract contract TestBase is + Test, + TestBaseForksConstants, + TestBaseRandomConstants, + TestHelpers, + LiFiDEXAggregatorDiamondTest, + ILiFi, + LiFiData +{ address internal _facetTestContractAddress; uint64 internal currentTxId; bytes32 internal nextUser = keccak256(abi.encodePacked("user address")); @@ -91,7 +102,6 @@ abstract contract TestBase is Test, DiamondTest, ILiFi, LiFiData { ERC20 internal usdt; ERC20 internal dai; ERC20 internal weth; - LiFiDiamond internal diamond; FeeCollector internal feeCollector; ILiFi.BridgeData internal bridgeData; LibSwap.SwapData[] internal swapData; @@ -101,9 +111,6 @@ abstract contract TestBase is Test, DiamondTest, ILiFi, LiFiData { // tokenAddress => userAddress => balance mapping(address => mapping(address => uint256)) internal initialBalances; uint256 internal addToMessageValue; - // set these custom values in your test file to - uint256 internal customBlockNumberForForking; - string internal customRpcUrlForForking; string internal logFilePath; // EVENTS @@ -121,95 +128,6 @@ abstract contract TestBase is Test, DiamondTest, ILiFi, LiFiData { error NativeBridgeFailed(); error ERC20BridgeFailed(); - // CONSTANTS - // Forking - uint256 internal constant DEFAULT_BLOCK_NUMBER_MAINNET = 15588208; - - // WALLET ADDRESSES (all networks) - address internal constant REFUND_WALLET = - 0x317F8d18FB16E49a958Becd0EA72f8E153d25654; - address internal constant WITHDRAW_WALLET = - 0x08647cc950813966142A416D40C382e2c5DB73bB; - - // Contract addresses (MAINNET) - // solhint-disable var-name-mixedcase - address internal ADDRESS_UNISWAP = - 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - // solhint-disable var-name-mixedcase - address internal ADDRESS_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - // solhint-disable var-name-mixedcase - address internal ADDRESS_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; - // solhint-disable var-name-mixedcase - address internal ADDRESS_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; - // solhint-disable var-name-mixedcase - address internal ADDRESS_WRAPPED_NATIVE = - 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - // Contract addresses (ARBITRUM) - address internal constant ADDRESS_UNISWAP_ARB = - 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24; - address internal constant ADDRESS_SUSHISWAP_ARB = - 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; - address internal constant ADDRESS_USDC_ARB = - 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8; - address internal constant ADDRESS_USDT_ARB = - 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9; - address internal constant ADDRESS_DAI_ARB = - 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; - address internal constant ADDRESS_WRAPPED_NATIVE_ARB = - 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; - // Contract addresses (POLYGON) - address internal constant ADDRESS_UNISWAP_POL = - 0xedf6066a2b290C185783862C7F4776A2C8077AD1; - address internal constant ADDRESS_SUSHISWAP_POL = - 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; - address internal constant ADDRESS_USDC_POL = - 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; // Circle USDC, decimals: 6 - address internal constant ADDRESS_USDCE_POL = - 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; // USDC.e - address internal constant ADDRESS_USDT_POL = - 0xc2132D05D31c914a87C6611C10748AEb04B58e8F; - address internal constant ADDRESS_DAI_POL = - 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063; - address internal constant ADDRESS_WETH_POL = - 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; - address internal constant ADDRESS_WRAPPED_NATIVE_POL = - 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; // WMATIC - // Contract addresses (BASE) - address internal constant ADDRESS_UNISWAP_BASE = - 0x6BDED42c6DA8FBf0d2bA55B2fa120C5e0c8D7891; - address internal constant ADDRESS_USDC_BASE = - 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; - address internal constant ADDRESS_USDT_BASE = - 0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2; - address internal constant ADDRESS_DAI_BASE = - 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb; - address internal constant ADDRESS_WRAPPED_NATIVE_BASE = - 0x4200000000000000000000000000000000000006; - // Contract addresses (OPTIMISM) - address internal constant ADDRESS_UNISWAP_OPTIMISM = - 0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2; - address internal constant ADDRESS_USDC_OPTIMISM = - 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; - address internal constant ADDRESS_USDT_OPTIMISM = - 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; - address internal constant ADDRESS_DAI_OPTIMISM = - 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; - address internal constant ADDRESS_WRAPPED_NATIVE_OPTIMISM = - 0x4200000000000000000000000000000000000006; - // User accounts (Whales: ETH only) - address internal constant USER_SENDER = address(0xabc123456); // initially funded with 100,000 DAI, USDC, USDT, WETH & ETHER - address internal constant USER_RECEIVER = address(0xabc654321); - address internal constant USER_REFUND = address(0xabcdef281); - address internal constant USER_PAUSER = address(0xdeadbeef); - address internal constant USER_DIAMOND_OWNER = - 0x5042255A3F3FD7727e419CeA387cAFDfad3C3aF8; - address internal constant USER_USDC_WHALE = - 0x72A53cDBBcc1b9efa39c834A540550e23463AAcB; - address internal constant USER_DAI_WHALE = - 0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016; - address internal constant USER_WETH_WHALE = - 0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E; - // MODIFIERS //@dev token == address(0) => check balance of native token @@ -314,9 +232,8 @@ abstract contract TestBase is Test, DiamondTest, ILiFi, LiFiData { dai = ERC20(ADDRESS_DAI); weth = ERC20(ADDRESS_WRAPPED_NATIVE); - // deploy & configure diamond - diamond = createDiamond(USER_DIAMOND_OWNER, USER_PAUSER); - + // deploy & configure LiFiDiamond and LiFiDEXAggregatorDiamond + LiFiDEXAggregatorDiamondTest.setUp(); // deploy feeCollector feeCollector = new FeeCollector(USER_DIAMOND_OWNER); @@ -360,17 +277,6 @@ abstract contract TestBase is Test, DiamondTest, ILiFi, LiFiData { vm.label(facetAddress, facetName); } - function fork() internal virtual { - string memory rpcUrl = bytes(customRpcUrlForForking).length > 0 - ? vm.envString(customRpcUrlForForking) - : vm.envString("ETH_NODE_URI_MAINNET"); - uint256 blockNumber = customBlockNumberForForking > 0 - ? customBlockNumberForForking - : 14847528; - - vm.createSelectFork(rpcUrl, blockNumber); - } - function setDefaultBridgeData() internal { bridgeData = ILiFi.BridgeData({ transactionId: "someId", diff --git a/test/solidity/utils/TestBaseForksConstants.sol b/test/solidity/utils/TestBaseForksConstants.sol new file mode 100644 index 000000000..cc990a6c6 --- /dev/null +++ b/test/solidity/utils/TestBaseForksConstants.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.17; + +abstract contract TestBaseForksConstants { + // Forking + uint256 internal constant DEFAULT_BLOCK_NUMBER_MAINNET = 15588208; + + // WALLET ADDRESSES (all networks) + address internal constant REFUND_WALLET = + 0x317F8d18FB16E49a958Becd0EA72f8E153d25654; + address internal constant WITHDRAW_WALLET = + 0x08647cc950813966142A416D40C382e2c5DB73bB; + + // Contract addresses (MAINNET) + // solhint-disable var-name-mixedcase + address internal ADDRESS_UNISWAP = + 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + // solhint-disable var-name-mixedcase + address internal ADDRESS_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + // solhint-disable var-name-mixedcase + address internal ADDRESS_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + // solhint-disable var-name-mixedcase + address internal ADDRESS_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + // solhint-disable var-name-mixedcase + address internal ADDRESS_WRAPPED_NATIVE = + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + // Contract addresses (ARBITRUM) + address internal constant ADDRESS_UNISWAP_ARB = + 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24; + address internal constant ADDRESS_SUSHISWAP_ARB = + 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; + address internal constant ADDRESS_USDC_ARB = + 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8; + address internal constant ADDRESS_USDT_ARB = + 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9; + address internal constant ADDRESS_DAI_ARB = + 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; + address internal constant ADDRESS_WRAPPED_NATIVE_ARB = + 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1; + // Contract addresses (POLYGON) + address internal constant ADDRESS_UNISWAP_POL = + 0xedf6066a2b290C185783862C7F4776A2C8077AD1; + address internal constant ADDRESS_SUSHISWAP_POL = + 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; + address internal constant ADDRESS_USDC_POL = + 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359; // Circle USDC, decimals: 6 + address internal constant ADDRESS_USDCE_POL = + 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; // USDC.e + address internal constant ADDRESS_USDT_POL = + 0xc2132D05D31c914a87C6611C10748AEb04B58e8F; + address internal constant ADDRESS_DAI_POL = + 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063; + address internal constant ADDRESS_WETH_POL = + 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; + address internal constant ADDRESS_WRAPPED_NATIVE_POL = + 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270; // WMATIC + // Contract addresses (BASE) + address internal constant ADDRESS_UNISWAP_BASE = + 0x6BDED42c6DA8FBf0d2bA55B2fa120C5e0c8D7891; + address internal constant ADDRESS_USDC_BASE = + 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address internal constant ADDRESS_USDT_BASE = + 0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2; + address internal constant ADDRESS_DAI_BASE = + 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb; + address internal constant ADDRESS_WRAPPED_NATIVE_BASE = + 0x4200000000000000000000000000000000000006; + // Contract addresses (OPTIMISM) + address internal constant ADDRESS_UNISWAP_OPTIMISM = + 0x4A7b5Da61326A6379179b40d00F57E5bbDC962c2; + address internal constant ADDRESS_USDC_OPTIMISM = + 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address internal constant ADDRESS_USDT_OPTIMISM = + 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; + address internal constant ADDRESS_DAI_OPTIMISM = + 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; + address internal constant ADDRESS_WRAPPED_NATIVE_OPTIMISM = + 0x4200000000000000000000000000000000000006; + // User accounts (Whales: ETH only) + address internal constant USER_USDC_WHALE = + 0x72A53cDBBcc1b9efa39c834A540550e23463AAcB; + address internal constant USER_DAI_WHALE = + 0x4aa42145Aa6Ebf72e164C9bBC74fbD3788045016; + address internal constant USER_WETH_WHALE = + 0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E; +} diff --git a/test/solidity/utils/TestBaseRandomConstants.sol b/test/solidity/utils/TestBaseRandomConstants.sol new file mode 100644 index 000000000..4fad2acdd --- /dev/null +++ b/test/solidity/utils/TestBaseRandomConstants.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +abstract contract TestBaseRandomConstants { + address internal constant USER_SENDER = address(0xabc123456); + address internal constant USER_RECEIVER = address(0xabc654321); + address internal constant USER_REFUND = address(0xabcdef281); + address internal constant USER_PAUSER = address(0xdeadbeef); + address internal constant USER_DIAMOND_OWNER = + 0x5042255A3F3FD7727e419CeA387cAFDfad3C3aF8; +} diff --git a/test/solidity/utils/TestHelpers.sol b/test/solidity/utils/TestHelpers.sol index e28f1b1d9..7ce4428d1 100644 --- a/test/solidity/utils/TestHelpers.sol +++ b/test/solidity/utils/TestHelpers.sol @@ -4,10 +4,14 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { MockUniswapDEX } from "./MockUniswapDEX.sol"; +import { TestBaseForksConstants } from "./TestBaseForksConstants.sol"; import { TestWhitelistManagerBase } from "./TestWhitelistManagerBase.sol"; //common utilities for forge tests -contract TestHelpers is Test { +contract TestHelpers is Test, TestBaseForksConstants { + uint256 internal customBlockNumberForForking; + string internal customRpcUrlForForking; + /// @notice will deploy and fund a mock DEX that can simulate the following behaviour for both ERC20/Native: /// positive slippage#1: uses less input tokens as expected /// positive slippage#2: returns more output tokens as expected @@ -48,13 +52,45 @@ contract TestHelpers is Test { amountInActual ); // whitelist DEX & function selector - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.swapTokensForExactTokens.selector); - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.swapExactTokensForTokens.selector); - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.swapETHForExactTokens.selector); - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.swapExactETHForTokens.selector); - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.swapExactTokensForETH.selector); - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.swapTokensForExactETH.selector); - TestWhitelistManagerBase(diamond).addAllowedContractSelector(address(mockDex), mockDex.mockSwapWillRevertWithReason.selector); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.swapTokensForExactTokens.selector + ); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.swapExactTokensForTokens.selector + ); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.swapETHForExactTokens.selector + ); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.swapExactETHForTokens.selector + ); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.swapExactTokensForETH.selector + ); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.swapTokensForExactETH.selector + ); + TestWhitelistManagerBase(diamond).addAllowedContractSelector( + address(mockDex), + mockDex.mockSwapWillRevertWithReason.selector + ); + } + + function fork() internal virtual { + string memory rpcUrl = bytes(customRpcUrlForForking).length > 0 + ? vm.envString(customRpcUrlForForking) + : vm.envString("ETH_NODE_URI_MAINNET"); + uint256 blockNumber = customBlockNumberForForking > 0 + ? customBlockNumberForForking + : DEFAULT_BLOCK_NUMBER_MAINNET; + + vm.createSelectFork(rpcUrl, blockNumber); } }