diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 8fdc1ee..1c5be96 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -17,4 +17,4 @@ jobs: - uses: crytic/slither-action@f197989dea5b53e986d0f88c60a034ddd77ec9a8 with: target: 'foundry/' - slither-args: '--filter-paths foundry/lib/' + slither-args: '--filter-paths foundry/lib/' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ffaa1..2cebf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [0.77.0](https://github.com/propeller-heads/tycho-execution/compare/0.76.0...0.77.0) (2025-04-07) + + +### Features + +* add curve executor with router tests ([7cde513](https://github.com/propeller-heads/tycho-execution/commit/7cde5130d6038916dcb4a6a96c723c366b90da12)) +* allow executor to do native swaps, add diff pool type tests ([93bdc86](https://github.com/propeller-heads/tycho-execution/commit/93bdc86dc665e02d877bec1b749ee5cf7a399e32)) +* Refactor Curve Executor not to use the router ([9f21842](https://github.com/propeller-heads/tycho-execution/commit/9f2184258aab968f7d83df8a443c1a77b87a3e4c)) + + +### Bug Fixes + +* fix slither CI action ([42d1ab3](https://github.com/propeller-heads/tycho-execution/commit/42d1ab36fd71af7a10d17120e5f14edce9f6422a)) +* Improve curve executor tests and docstrings ([f468a78](https://github.com/propeller-heads/tycho-execution/commit/f468a7831a86eef96682504bc93207f33b28cf17)) +* Remove unnecessary test method ([49aefc8](https://github.com/propeller-heads/tycho-execution/commit/49aefc8c2ab25864b4056f44738a96c3905f8396)) +* resolve pr comments ([9e2a9f5](https://github.com/propeller-heads/tycho-execution/commit/9e2a9f5329f798990d4dde3e7e76af4896aef6f6)) + ## [0.76.0](https://github.com/propeller-heads/tycho-execution/compare/0.75.1...0.76.0) (2025-04-03) diff --git a/Cargo.lock b/Cargo.lock index 7d69956..8df5aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4341,7 +4341,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.76.0" +version = "0.77.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 3979f0d..c5bb0d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.76.0" +version = "0.77.0" edition = "2021" description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors." repository = "https://github.com/propeller-heads/tycho-execution" diff --git a/foundry/src/executors/CurveExecutor.sol b/foundry/src/executors/CurveExecutor.sol new file mode 100644 index 0000000..6d98777 --- /dev/null +++ b/foundry/src/executors/CurveExecutor.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +error CurveExecutor__InvalidAddresses(); + +interface CryptoPool { + // slither-disable-next-line naming-convention + function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) + external + payable; +} + +interface StablePool { + // slither-disable-next-line naming-convention + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) + external + payable; +} + +interface CryptoPoolETH { + // slither-disable-start naming-convention + function exchange( + uint256 i, + uint256 j, + uint256 dx, + uint256 min_dy, + bool use_eth + ) external payable; + // slither-disable-end naming-convention +} + +contract CurveExecutor is IExecutor { + using SafeERC20 for IERC20; + + address public immutable nativeToken; + + constructor(address _nativeToken) { + if (_nativeToken == address(0)) { + revert CurveExecutor__InvalidAddresses(); + } + nativeToken = _nativeToken; + } + + // slither-disable-next-line locked-ether + function swap(uint256 amountIn, bytes calldata data) + external + payable + returns (uint256) + { + ( + address tokenIn, + address tokenOut, + address pool, + uint8 poolType, + int128 i, + int128 j, + bool tokenApprovalNeeded + ) = _decodeData(data); + + if (tokenApprovalNeeded && tokenIn != nativeToken) { + // slither-disable-next-line unused-return + IERC20(tokenIn).approve(address(pool), type(uint256).max); + } + + /// Inspired by Curve's router contract: https://github.com/curvefi/curve-router-ng/blob/9ab006ca848fc7f1995b6fbbecfecc1e0eb29e2a/contracts/Router.vy#L44 + uint256 balanceBefore = _balanceOf(tokenOut); + + uint256 ethAmount = 0; + if (tokenIn == nativeToken) { + ethAmount = amountIn; + } + + if (poolType == 1 || poolType == 10) { + // stable and stable_ng + // slither-disable-next-line arbitrary-send-eth + StablePool(pool).exchange{value: ethAmount}(i, j, amountIn, 0); + } else { + // crypto or llamma + if (tokenIn == nativeToken || tokenOut == nativeToken) { + // slither-disable-next-line arbitrary-send-eth + CryptoPoolETH(pool).exchange{value: ethAmount}( + uint256(int256(i)), uint256(int256(j)), amountIn, 0, true + ); + } else { + CryptoPool(pool).exchange( + uint256(int256(i)), uint256(int256(j)), amountIn, 0 + ); + } + } + + uint256 balanceAfter = _balanceOf(tokenOut); + return balanceAfter - balanceBefore; + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + address tokenIn, + address tokenOut, + address pool, + uint8 poolType, + int128 i, + int128 j, + bool tokenApprovalNeeded + ) + { + tokenIn = address(bytes20(data[0:20])); + tokenOut = address(bytes20(data[20:40])); + pool = address(bytes20(data[40:60])); + poolType = uint8(data[60]); + i = int128(uint128(uint8(data[61]))); + j = int128(uint128(uint8(data[62]))); + tokenApprovalNeeded = data[63] != 0; + } + + receive() external payable { + require(msg.sender.code.length != 0); + } + + function _balanceOf(address token) + internal + view + returns (uint256 balance) + { + balance = token == nativeToken + ? address(this).balance + : IERC20(token).balanceOf(address(this)); + } +} diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 1a1fff7..8ff3dc4 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -30,6 +30,7 @@ contract Constants is Test, BaseConstants { address UNPAUSER = makeAddr("unpauser"); // Assets + address ETH_ADDR = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D); @@ -39,7 +40,21 @@ contract Constants is Test, BaseConstants { address USDE_ADDR = address(0x4c9EDD5852cd905f086C759E8383e09bff1E68B3); address USDT_ADDR = address(0xdAC17F958D2ee523a2206206994597C13D831ec7); address PEPE_ADDR = address(0x6982508145454Ce325dDbE47a25d4ec3d2311933); - + address STETH_ADDR = address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); + address LUSD_ADDR = address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0); + address LDO_ADDR = address(0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32); + address CRV_ADDR = address(0xD533a949740bb3306d119CC777fa900bA034cd52); + address ADAI_ADDR = address(0x028171bCA77440897B824Ca71D1c56caC55b68A3); + address AUSDC_ADDR = address(0xBcca60bB61934080951369a648Fb03DF4F96263C); + address SUSD_ADDR = address(0x57Ab1ec28D129707052df4dF418D58a2D46d5f51); + address FRAX_ADDR = address(0x853d955aCEf822Db058eb8505911ED77F175b99e); + address DOLA_ADDR = address(0x865377367054516e17014CcdED1e7d814EDC9ce4); + address XYO_ADDR = address(0x55296f69f40Ea6d20E478533C15A6B08B654E758); + address UWU_ADDR = address(0x55C08ca52497e2f1534B59E2917BF524D4765257); + address CRVUSD_ADDR = address(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E); + address WSTTAO_ADDR = address(0xe9633C52f4c8B7BDeb08c4A7fE8a5c1B84AFCf67); + address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44); + address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3); // Uniswap v2 address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5; @@ -76,6 +91,27 @@ contract Constants is Test, BaseConstants { address PANCAKESWAPV3_DEPLOYER_ETHEREUM = 0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9; + // Curve + address TRIPOOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; + address TRICRYPTO_POOL = 0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B; + address STETH_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; + address LUSD_POOL = 0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA; + address CPOOL = 0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56; + address LDO_POOL = 0x9409280DC1e6D33AB7A8C6EC03e5763FB61772B5; + address CRV_POOL = 0x8301AE4fc9c624d1D396cbDAa1ed877821D7C511; + address AAVE_POOL = 0xDeBF20617708857ebe4F679508E7b7863a8A8EeE; + address FRAXPYUSD_POOL = address(0xA5588F7cdf560811710A2D82D3C9c99769DB1Dcb); + address TRICRYPTO2_POOL = 0xD51a44d3FaE010294C616388b506AcdA1bfAAE46; + address SUSD_POOL = 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD; + address FRAX_USDC_POOL = 0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2; + address USDE_USDC_POOL = 0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72; + address DOLA_FRAXPYUSD_POOL = 0xef484de8C07B6e2d732A92B5F78e81B38f99f95E; + address ETH_XYO_POOL = 0x99e09ee2d6Bb16c0F5ADDfEA649dbB2C1d524624; + address UWU_WETH_POOL = 0x77146B0a1d08B6844376dF6d9da99bA7F1b19e71; + address CRVUSD_USDT_POOL = 0x390f3595bCa2Df7d23783dFd126427CCeb997BF4; + address WSTTAO_WTAO_POOL = 0xf2DCf6336D8250754B4527f57b275b19c8D5CF88; + address BSGG_USDT_POOL = 0x5500307Bcf134E5851FB4D7D8D1Dc556dCdB84B4; + // Uniswap universal router address UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af; @@ -94,6 +130,9 @@ contract Constants is Test, BaseConstants { bytes32 PANCAKEV3_POOL_CODE_INIT_HASH = 0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2; + // Curve meta registry + address CURVE_META_REGISTRY = 0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC; + /** * @dev Deploys a dummy contract with non-empty bytecode */ diff --git a/foundry/test/executors/CurveExecutor.t.sol b/foundry/test/executors/CurveExecutor.t.sol new file mode 100644 index 0000000..c3b9cad --- /dev/null +++ b/foundry/test/executors/CurveExecutor.t.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/CurveExecutor.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "../Constants.sol"; + +interface ICurvePool { + function coins(uint256 i) external view returns (address); +} + +// Curve pool registry +// This is the registry that contains the information about the pool +// The naming convention is different because it is in vyper +interface MetaRegistry { + function get_n_coins(address pool) external view returns (uint256); + + function get_coin_indices(address pool, address from, address to) + external + view + returns (int128, int128, bool); +} + +interface IAaveLendingPool { + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function withdraw(address asset, uint256 amount, address to) + external + returns (uint256); +} + +contract CurveExecutorExposed is CurveExecutor { + constructor(address _nativeToken) CurveExecutor(_nativeToken) {} + + function decodeData(bytes calldata data) + external + pure + returns ( + address tokenIn, + address tokenOut, + address pool, + uint8 poolType, + int128 i, + int128 j, + bool tokenApprovalNeeded + ) + { + return _decodeData(data); + } +} + +contract CurveExecutorTest is Test, Constants { + using SafeERC20 for IERC20; + + CurveExecutorExposed curveExecutorExposed; + MetaRegistry metaRegistry; + + function setUp() public { + uint256 forkBlock = 22031795; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + curveExecutorExposed = new CurveExecutorExposed(ETH_ADDR); + metaRegistry = MetaRegistry(CURVE_META_REGISTRY); + } + + function testDecodeParams() public view { + bytes memory data = abi.encodePacked( + WETH_ADDR, + USDC_ADDR, + TRICRYPTO_POOL, + uint8(3), + uint8(2), + uint8(0), + true + ); + + ( + address tokenIn, + address tokenOut, + address pool, + uint8 poolType, + int128 i, + int128 j, + bool tokenApprovalNeeded + ) = curveExecutorExposed.decodeData(data); + + assertEq(tokenIn, WETH_ADDR); + assertEq(tokenOut, USDC_ADDR); + assertEq(pool, TRICRYPTO_POOL); + assertEq(poolType, 3); + assertEq(i, 2); + assertEq(j, 0); + assertEq(tokenApprovalNeeded, true); + } + + function testTriPool() public { + // Swapping DAI -> USDC on TriPool 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7 + uint256 amountIn = 1 ether; + deal(DAI_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(DAI_ADDR, USDC_ADDR, TRIPOOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 999797); + assertEq( + IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testStEthPool() public { + // Swapping ETH -> stETH on StEthPool 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022 + uint256 amountIn = 1 ether; + deal(address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(ETH_ADDR, STETH_ADDR, STETH_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 1001072414418410897); + assertEq( + IERC20(STETH_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testTricrypto2Pool() public { + // Swapping WETH -> WBTC on Tricrypto2Pool 0xD51a44d3FaE010294C616388b506AcdA1bfAAE46 + uint256 amountIn = 1 ether; + deal(WETH_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(WETH_ADDR, WBTC_ADDR, TRICRYPTO2_POOL, 3); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 2279618); + assertEq( + IERC20(WBTC_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testSUSDPool() public { + // Swapping USDC -> SUSD on SUSDPool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD + uint256 amountIn = 100 * 10 ** 6; + deal(USDC_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(USDC_ADDR, SUSD_ADDR, SUSD_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 100488101605550214590); + assertEq( + IERC20(SUSD_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testFraxUsdcPool() public { + // Swapping FRAX -> USDC on FraxUsdcPool 0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2 + uint256 amountIn = 1 ether; + deal(FRAX_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(FRAX_ADDR, USDC_ADDR, FRAX_USDC_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 998097); + assertEq( + IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testUsdeUsdcPool() public { + // Swapping USDC -> USDE on a CryptoSwapNG, deployed by factory 0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf (plain pool) + uint256 amountIn = 100 * 10 ** 6; + deal(USDC_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(USDC_ADDR, USDE_ADDR, USDE_USDC_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 100064812138999986170); + assertEq( + IERC20(USDE_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testDolaFraxPyusdPool() public { + // Swapping DOLA -> FRAXPYUSD on a CryptoSwapNG, deployed by factory 0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf (meta pool) + uint256 amountIn = 100 * 10 ** 6; + deal(DOLA_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = + _getData(DOLA_ADDR, FRAXPYUSD_POOL, DOLA_FRAXPYUSD_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 99688992); + assertEq( + IERC20(FRAXPYUSD_POOL).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testCryptoPoolWithETH() public { + // Swapping XYO -> ETH on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99 + uint256 amountIn = 1 ether; + uint256 initialBalance = address(curveExecutorExposed).balance; // this address already has some ETH assigned to it + deal(XYO_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(XYO_ADDR, ETH_ADDR, ETH_XYO_POOL, 2); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 6081816039338); + assertEq( + address(curveExecutorExposed).balance, initialBalance + amountOut + ); + } + + function testCryptoPool() public { + // Swapping BSGG -> USDT on a CryptoPool, deployed by factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99 + uint256 amountIn = 1000 ether; + deal(BSGG_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(BSGG_ADDR, USDT_ADDR, BSGG_USDT_POOL, 2); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 23429); + assertEq( + IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testTricryptoPool() public { + // Swapping WETH -> USDC on a Tricrypto pool, deployed by factory 0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963 + uint256 amountIn = 1 ether; + deal(WETH_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(WETH_ADDR, USDC_ADDR, TRICRYPTO_POOL, 2); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 1861130974); + assertEq( + IERC20(USDC_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testTwoCryptoPool() public { + // Swapping UWU -> WETH on a Twocrypto pool, deployed by factory 0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f + uint256 amountIn = 1 ether; + deal(UWU_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = _getData(UWU_ADDR, WETH_ADDR, UWU_WETH_POOL, 2); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 2873786684675); + assertEq( + IERC20(WETH_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testStableSwapPool() public { + // Swapping CRVUSD -> USDT on a StableSwap pool, deployed by factory 0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d (plain pool) + uint256 amountIn = 1 ether; + deal(CRVUSD_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = + _getData(CRVUSD_ADDR, USDT_ADDR, CRVUSD_USDT_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 999910); + assertEq( + IERC20(USDT_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function testMetaPool() public { + // Swapping WTAO -> WSTTAO on a MetaPool deployed by factory 0xB9fC157394Af804a3578134A6585C0dc9cc990d4 (plain pool) + uint256 amountIn = 100 * 10 ** 9; // 9 decimals + deal(WTAO_ADDR, address(curveExecutorExposed), amountIn); + + bytes memory data = + _getData(WTAO_ADDR, WSTTAO_ADDR, WSTTAO_WTAO_POOL, 1); + + uint256 amountOut = curveExecutorExposed.swap(amountIn, data); + + assertEq(amountOut, 32797923610); + assertEq( + IERC20(WSTTAO_ADDR).balanceOf(address(curveExecutorExposed)), + amountOut + ); + } + + function _getData( + address tokenIn, + address tokenOut, + address pool, + uint8 poolType + ) internal view returns (bytes memory data) { + (int128 i, int128 j) = _getIndexes(tokenIn, tokenOut, pool); + data = abi.encodePacked( + tokenIn, + tokenOut, + pool, + poolType, + uint8(uint256(uint128(i))), + uint8(uint256(uint128(j))), + true + ); + } + + function _getIndexes(address tokenIn, address tokenOut, address pool) + internal + view + returns (int128, int128) + { + (int128 coinInIndex, int128 coinOutIndex,) = + metaRegistry.get_coin_indices(pool, tokenIn, tokenOut); + return (coinInIndex, coinOutIndex); + } +}