diff --git a/foundry/src/executors/MaverickV2Executor.sol b/foundry/src/executors/MaverickV2Executor.sol new file mode 100644 index 0000000..a5971c2 --- /dev/null +++ b/foundry/src/executors/MaverickV2Executor.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +error MaverickV2Executor__InvalidDataLength(); +error MaverickV2Executor__InvalidTarget(); +error MaverickV2Executor__InvalidFactory(); + +contract MaverickV2Executor is IExecutor { + using SafeERC20 for IERC20; + + address public immutable factory; + address private immutable self; + + constructor(address _factory) { + if (_factory == address(0)) { + revert MaverickV2Executor__InvalidFactory(); + } + factory = _factory; + self = address(this); + } + + // slither-disable-next-line locked-ether + function swap(uint256 givenAmount, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { + address target; + address receiver; + IERC20 tokenIn; + + (tokenIn, target, receiver) = _decodeData(data); + + _verifyPairAddress(target); + IMaverickV2Pool pool = IMaverickV2Pool(target); + + bool isTokenAIn = pool.tokenA() == tokenIn; + int32 tickLimit = isTokenAIn ? type(int32).max : type(int32).min; + IMaverickV2Pool.SwapParams memory swapParams = IMaverickV2Pool + .SwapParams({ + amount: givenAmount, + tokenAIn: isTokenAIn, + exactOutput: false, + tickLimit: tickLimit + }); + IERC20(tokenIn).safeTransfer(target, givenAmount); + (, calculatedAmount) = pool.swap(receiver, swapParams, ""); + } + + function _decodeData(bytes calldata data) + internal + pure + returns (IERC20 inToken, address target, address receiver) + { + if (data.length != 60) { + revert MaverickV2Executor__InvalidDataLength(); + } + inToken = IERC20(address(bytes20(data[0:20]))); + target = address(bytes20(data[20:40])); + receiver = address(bytes20(data[40:60])); + } + + function _verifyPairAddress(address target) internal view { + if (!IMaverickV2Factory(factory).isFactoryPool(IMaverickV2Pool(target))) { + revert MaverickV2Executor__InvalidTarget(); + } + } +} + +interface IMaverickV2Factory { + function isFactoryPool(IMaverickV2Pool pool) external view returns (bool); +} + +interface IMaverickV2Pool { + struct SwapParams { + uint256 amount; + bool tokenAIn; + bool exactOutput; + int32 tickLimit; + } + + function swap( + address recipient, + SwapParams memory params, + bytes calldata data + ) external returns (uint256 amountIn, uint256 amountOut); + + function tokenA() external view returns (IERC20); + function tokenB() external view returns (IERC20); +} diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 9b0f488..51b292d 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -56,6 +56,12 @@ contract Constants is Test, BaseConstants { address WSTTAO_ADDR = address(0xe9633C52f4c8B7BDeb08c4A7fE8a5c1B84AFCf67); address WTAO_ADDR = address(0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44); address BSGG_ADDR = address(0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3); + address GHO_ADDR = address(0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f); + + // Maverick v2 + address MAVERICK_V2_FACTORY = 0x0A7e848Aca42d879EF06507Fca0E7b33A0a63c1e; + address GHO_USDC_POOL = 0x14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67; + // Uniswap v2 address WETH_DAI_POOL = 0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11; address DAI_USDC_POOL = 0xAE461cA67B15dc8dc81CE7615e0320dA1A9aB8D5; diff --git a/foundry/test/executors/MaverickV2Executor.t.sol b/foundry/test/executors/MaverickV2Executor.t.sol new file mode 100644 index 0000000..8d7fe6b --- /dev/null +++ b/foundry/test/executors/MaverickV2Executor.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@src/executors/MaverickV2Executor.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {Constants} from "../Constants.sol"; + +contract MaverickV2ExecutorExposed is MaverickV2Executor { + constructor(address _factory) MaverickV2Executor(_factory) {} + function decodeParams(bytes calldata data) + external + pure + returns ( + IERC20 tokenIn, + address target, + address receiver + ) + { + return _decodeData(data); + } +} + +contract MaverickV2ExecutorTest is + Test, + Constants +{ + using SafeERC20 for IERC20; + + MaverickV2ExecutorExposed maverickV2Exposed; + IERC20 GHO = IERC20(GHO_ADDR); + IERC20 USDC = IERC20(USDC_ADDR); + + function setUp() public { + uint256 forkBlock = 20127232; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + maverickV2Exposed = new MaverickV2ExecutorExposed(MAVERICK_V2_FACTORY); + } + + function testDecodeParams() public view { + bytes memory params = abi.encodePacked( + GHO_ADDR, GHO_USDC_POOL, address(2) + ); + + ( + IERC20 tokenIn, + address target, + address receiver + ) = maverickV2Exposed.decodeParams(params); + + assertEq(address(tokenIn), GHO_ADDR); + assertEq(target, GHO_USDC_POOL); + assertEq(receiver, address(2)); + } + + function testDecodeParamsInvalidDataLength() public { + bytes memory invalidParams = + abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, address(2), true); + + vm.expectRevert(MaverickV2Executor__InvalidDataLength.selector); + maverickV2Exposed.decodeParams(invalidParams); + } + + function testSwap() public { + uint256 amountIn = 10 ** 18; + bytes memory protocolData = + abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, address(2)); + + deal(GHO_ADDR, address(maverickV2Exposed), amountIn); + uint256 balanceBefore = GHO.balanceOf(BOB); + + uint256 amountOut = maverickV2Exposed.swap(amountIn, protocolData); + + uint256 balanceAfter = GHO.balanceOf(BOB); + assertGt(balanceAfter, balanceBefore); + assertEq(balanceAfter - balanceBefore, amountOut); + } +}