feat: Transfer Optimizations in MaverickV2

- Also added integration test to test the optimizations, where we can see the in and out transfers being optimized if we enable verbose foundry testing
- Fixed typo in swap encoder builder initialization
This commit is contained in:
TAMARA LIPOWSKI
2025-04-29 15:36:21 -04:00
parent b6a3ce624d
commit bcef8f69f6
10 changed files with 152 additions and 24 deletions

View File

@@ -8,6 +8,7 @@
"uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a",
"vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1",
"ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598", "ekubo_v2": "0xa0Cb889707d426A7A386870A03bc70d1b0697598",
"vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211" "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211",
"vm:maverick_v2": "0xA4AD4f68d0b91CFD19687c881e50f3A00242828c"
} }
} }

View File

@@ -3,18 +3,19 @@ pragma solidity ^0.8.26;
import "@interfaces/IExecutor.sol"; import "@interfaces/IExecutor.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./TokenTransfer.sol";
error MaverickV2Executor__InvalidDataLength(); error MaverickV2Executor__InvalidDataLength();
error MaverickV2Executor__InvalidTarget(); error MaverickV2Executor__InvalidTarget();
error MaverickV2Executor__InvalidFactory(); error MaverickV2Executor__InvalidFactory();
contract MaverickV2Executor is IExecutor { contract MaverickV2Executor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
address public immutable factory; address public immutable factory;
address private immutable self; address private immutable self;
constructor(address _factory) { constructor(address _factory, address _permit2) TokenTransfer(_permit2) {
if (_factory == address(0)) { if (_factory == address(0)) {
revert MaverickV2Executor__InvalidFactory(); revert MaverickV2Executor__InvalidFactory();
} }
@@ -31,8 +32,9 @@ contract MaverickV2Executor is IExecutor {
address target; address target;
address receiver; address receiver;
IERC20 tokenIn; IERC20 tokenIn;
TransferType transferType;
(tokenIn, target, receiver) = _decodeData(data); (tokenIn, target, receiver, transferType) = _decodeData(data);
_verifyPairAddress(target); _verifyPairAddress(target);
IMaverickV2Pool pool = IMaverickV2Pool(target); IMaverickV2Pool pool = IMaverickV2Pool(target);
@@ -46,21 +48,31 @@ contract MaverickV2Executor is IExecutor {
exactOutput: false, exactOutput: false,
tickLimit: tickLimit tickLimit: tickLimit
}); });
IERC20(tokenIn).safeTransfer(target, givenAmount);
_transfer(
address(tokenIn), msg.sender, target, givenAmount, transferType
);
// slither-disable-next-line unused-return
(, calculatedAmount) = pool.swap(receiver, swapParams, ""); (, calculatedAmount) = pool.swap(receiver, swapParams, "");
} }
function _decodeData(bytes calldata data) function _decodeData(bytes calldata data)
internal internal
pure pure
returns (IERC20 inToken, address target, address receiver) returns (
IERC20 inToken,
address target,
address receiver,
TransferType transferType
)
{ {
if (data.length != 60) { if (data.length != 61) {
revert MaverickV2Executor__InvalidDataLength(); revert MaverickV2Executor__InvalidDataLength();
} }
inToken = IERC20(address(bytes20(data[0:20]))); inToken = IERC20(address(bytes20(data[0:20])));
target = address(bytes20(data[20:40])); target = address(bytes20(data[20:40]));
receiver = address(bytes20(data[40:60])); receiver = address(bytes20(data[40:60]));
transferType = TransferType(uint8(data[60]));
} }
function _verifyPairAddress(address target) internal view { function _verifyPairAddress(address target) internal view {

View File

@@ -186,6 +186,27 @@ contract TychoRouterTestProtocolIntegration is TychoRouterTestSetup {
assertEq(balanceAfter - balanceBefore, 1474406268748155809); assertEq(balanceAfter - balanceBefore, 1474406268748155809);
} }
function testSingleMaverickIntegration() public {
vm.stopPrank();
deal(GHO_ADDR, ALICE, 1 ether);
uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE);
// Approve permit2
vm.startPrank(ALICE);
IERC20(GHO_ADDR).approve(tychoRouterAddr, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_single_encoding_strategy_maverick");
(bool success,) = tychoRouterAddr.call(callData);
uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE);
assertTrue(success, "Call Failed");
assertGe(balanceAfter - balanceBefore, 999725);
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSingleEkuboIntegration() public { function testSingleEkuboIntegration() public {
vm.stopPrank(); vm.stopPrank();

View File

@@ -15,6 +15,7 @@ import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol";
import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol";
import {Permit2TestHelper} from "./Permit2TestHelper.sol"; import {Permit2TestHelper} from "./Permit2TestHelper.sol";
import "./TestUtils.sol"; import "./TestUtils.sol";
import {MaverickV2Executor} from "../src/executors/MaverickV2Executor.sol";
contract TychoRouterExposed is TychoRouter { contract TychoRouterExposed is TychoRouter {
constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {}
@@ -53,6 +54,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
BalancerV2Executor public balancerv2Executor; BalancerV2Executor public balancerv2Executor;
EkuboExecutor public ekuboExecutor; EkuboExecutor public ekuboExecutor;
CurveExecutor public curveExecutor; CurveExecutor public curveExecutor;
MaverickV2Executor public maverickv2Executor;
MockERC20[] tokens; MockERC20[] tokens;
function setUp() public { function setUp() public {
@@ -110,8 +112,10 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS);
ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS); ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS);
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
maverickv2Executor =
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
address[] memory executors = new address[](7); address[] memory executors = new address[](8);
executors[0] = address(usv2Executor); executors[0] = address(usv2Executor);
executors[1] = address(usv3Executor); executors[1] = address(usv3Executor);
executors[2] = address(pancakev3Executor); executors[2] = address(pancakev3Executor);
@@ -119,6 +123,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
executors[4] = address(balancerv2Executor); executors[4] = address(balancerv2Executor);
executors[5] = address(ekuboExecutor); executors[5] = address(ekuboExecutor);
executors[6] = address(curveExecutor); executors[6] = address(curveExecutor);
executors[7] = address(maverickv2Executor);
return executors; return executors;
} }

View File

@@ -24,3 +24,4 @@ test_encode_balancer_v2:c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754
test_ekubo_encode_swap_multi:00ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032 test_ekubo_encode_swap_multi:00ca4f73fe97d0b987a0d12b39bbd562c779bab6f60000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000001a36e2eb1c43200000032
test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c test_encode_uniswap_v4_sequential_swap:4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c
test_encode_uniswap_v4_simple_swap:4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7000064000001 test_encode_uniswap_v4_simple_swap:4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70100cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2dac17f958d2ee523a2206206994597c13d831ec7000064000001
test_single_encoding_strategy_maverick:20144a070000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000040d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000051a4ad4f68d0b91cfd19687c881e50f3a00242828c40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c67cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc201000000000000000000000000000000

View File

@@ -6,12 +6,19 @@ import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol"; import {Constants} from "../Constants.sol";
contract MaverickV2ExecutorExposed is MaverickV2Executor { contract MaverickV2ExecutorExposed is MaverickV2Executor {
constructor(address _factory) MaverickV2Executor(_factory) {} constructor(address _factory, address _permit2)
MaverickV2Executor(_factory, _permit2)
{}
function decodeParams(bytes calldata data) function decodeParams(bytes calldata data)
external external
pure pure
returns (IERC20 tokenIn, address target, address receiver) returns (
IERC20 tokenIn,
address target,
address receiver,
TransferType transferType
)
{ {
return _decodeData(data); return _decodeData(data);
} }
@@ -27,24 +34,37 @@ contract MaverickV2ExecutorTest is Test, Constants {
function setUp() public { function setUp() public {
uint256 forkBlock = 22096000; uint256 forkBlock = 22096000;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
maverickV2Exposed = new MaverickV2ExecutorExposed(MAVERICK_V2_FACTORY); maverickV2Exposed =
new MaverickV2ExecutorExposed(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
} }
function testDecodeParams() public view { function testDecodeParams() public view {
bytes memory params = bytes memory params = abi.encodePacked(
abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, address(2)); GHO_ADDR,
GHO_USDC_POOL,
address(2),
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
(IERC20 tokenIn, address target, address receiver) = (
maverickV2Exposed.decodeParams(params); IERC20 tokenIn,
address target,
address receiver,
TokenTransfer.TransferType transferType
) = maverickV2Exposed.decodeParams(params);
assertEq(address(tokenIn), GHO_ADDR); assertEq(address(tokenIn), GHO_ADDR);
assertEq(target, GHO_USDC_POOL); assertEq(target, GHO_USDC_POOL);
assertEq(receiver, address(2)); assertEq(receiver, address(2));
assertEq(
uint8(transferType),
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
} }
function testDecodeParamsInvalidDataLength() public { function testDecodeParamsInvalidDataLength() public {
bytes memory invalidParams = bytes memory invalidParams =
abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, address(2), true); abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, address(2));
vm.expectRevert(MaverickV2Executor__InvalidDataLength.selector); vm.expectRevert(MaverickV2Executor__InvalidDataLength.selector);
maverickV2Exposed.decodeParams(invalidParams); maverickV2Exposed.decodeParams(invalidParams);
@@ -52,8 +72,12 @@ contract MaverickV2ExecutorTest is Test, Constants {
function testSwap() public { function testSwap() public {
uint256 amountIn = 10e18; uint256 amountIn = 10e18;
bytes memory protocolData = bytes memory protocolData = abi.encodePacked(
abi.encodePacked(GHO_ADDR, GHO_USDC_POOL, BOB); GHO_ADDR,
GHO_USDC_POOL,
BOB,
TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL
);
deal(GHO_ADDR, address(maverickV2Exposed), amountIn); deal(GHO_ADDR, address(maverickV2Exposed), amountIn);
uint256 balanceBefore = USDC.balanceOf(BOB); uint256 balanceBefore = USDC.balanceOf(BOB);
@@ -68,20 +92,28 @@ contract MaverickV2ExecutorTest is Test, Constants {
function testDecodeIntegration() public view { function testDecodeIntegration() public view {
// Generated by the SwapEncoder - test_encode_maverick_v2 // Generated by the SwapEncoder - test_encode_maverick_v2
bytes memory protocolData = bytes memory protocolData =
hex"40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c671d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"; hex"40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c671d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00";
(IERC20 tokenIn, address pool, address receiver) = (
maverickV2Exposed.decodeParams(protocolData); IERC20 tokenIn,
address pool,
address receiver,
TokenTransfer.TransferType transferType
) = maverickV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), GHO_ADDR); assertEq(address(tokenIn), GHO_ADDR);
assertEq(pool, GHO_USDC_POOL); assertEq(pool, GHO_USDC_POOL);
assertEq(receiver, BOB); assertEq(receiver, BOB);
assertEq(
uint8(transferType),
uint8(TokenTransfer.TransferType.TRANSFER_TO_PROTOCOL)
);
} }
function testSwapIntegration() public { function testSwapIntegration() public {
// Generated by the SwapEncoder - test_encode_maverick_v2 // Generated by the SwapEncoder - test_encode_maverick_v2
bytes memory protocolData = bytes memory protocolData =
hex"40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c671d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"; hex"40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f14cf6d2fe3e1b326114b07d22a6f6bb59e346c671d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00";
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
deal(GHO_ADDR, address(maverickV2Exposed), amountIn); deal(GHO_ADDR, address(maverickV2Exposed), amountIn);

View File

@@ -20,7 +20,7 @@ pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(
/// These protocols need an external in transfer to the pool. This transfer can be from the router, /// These protocols need an external in transfer to the pool. This transfer can be from the router,
/// from the user or from the previous pool. Any protocols that are not defined here expect funds to /// from the user or from the previous pool. Any protocols that are not defined here expect funds to
/// be in the router at the time of swap and do the transfer themselves from msg.sender /// be in the router at the time of swap and do the transfer themselves from `msg.sender`
pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut set = HashSet::new(); let mut set = HashSet::new();
set.insert("uniswap_v2"); set.insert("uniswap_v2");
@@ -30,6 +30,7 @@ pub static IN_TRANSFER_REQUIRED_PROTOCOLS: LazyLock<HashSet<&'static str>> = Laz
set.insert("pancakeswap_v3"); set.insert("pancakeswap_v3");
set.insert("uniswap_v4"); set.insert("uniswap_v4");
set.insert("ekubo_v2"); set.insert("ekubo_v2");
set.insert("vm:maverick_v2");
set set
}); });

View File

@@ -2318,6 +2318,56 @@ mod tests {
write_calldata_to_file("test_single_encoding_strategy_ekubo", hex_calldata.as_str()); write_calldata_to_file("test_single_encoding_strategy_ekubo", hex_calldata.as_str());
} }
#[test]
fn test_single_encoding_strategy_maverick() {
// GHO -> (maverick) -> USDC
let maverick_pool = ProtocolComponent {
id: String::from("0x14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67"),
protocol_system: String::from("vm:maverick_v2"),
..Default::default()
};
let token_in = Bytes::from("0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f");
let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let swap = Swap {
component: maverick_pool,
token_in: token_in.clone(),
token_out: token_out.clone(),
split: 0f64,
};
let swap_encoder_registry = get_swap_encoder_registry();
let encoder = SingleSwapStrategyEncoder::new(
eth_chain(),
swap_encoder_registry,
None,
Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap(),
false,
)
.unwrap();
let solution = Solution {
exact_out: false,
given_token: token_in,
given_amount: BigUint::from_str("1_000000000000000000").unwrap(),
checked_token: token_out,
expected_amount: None,
checked_amount: Some(BigUint::from_str("1000").unwrap()),
slippage: None,
// Alice
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
swaps: vec![swap],
..Default::default()
};
let (calldata, _) = encoder
.encode_strategy(solution)
.unwrap();
let hex_calldata = encode(&calldata);
write_calldata_to_file("test_single_encoding_strategy_maverick", hex_calldata.as_str());
}
#[test] #[test]
fn test_single_encoding_strategy_usv4_eth_in() { fn test_single_encoding_strategy_usv4_eth_in() {
// Performs a single swap from ETH to PEPE using a USV4 pool // Performs a single swap from ETH to PEPE using a USV4 pool

View File

@@ -76,7 +76,7 @@ impl SwapEncoderBuilder {
"vm:curve" => { "vm:curve" => {
Ok(Box::new(CurveSwapEncoder::new(self.executor_address, self.chain, self.config)?)) Ok(Box::new(CurveSwapEncoder::new(self.executor_address, self.chain, self.config)?))
} }
"vm::maverick_v2" => Ok(Box::new(MaverickV2SwapEncoder::new( "vm:maverick_v2" => Ok(Box::new(MaverickV2SwapEncoder::new(
self.executor_address, self.executor_address,
self.chain, self.chain,
self.config, self.config,

View File

@@ -620,6 +620,7 @@ impl SwapEncoder for MaverickV2SwapEncoder {
bytes_to_address(&swap.token_in)?, bytes_to_address(&swap.token_in)?,
component_id, component_id,
bytes_to_address(&encoding_context.receiver)?, bytes_to_address(&encoding_context.receiver)?,
(encoding_context.transfer_type as u8).to_be_bytes(),
); );
Ok(args.abi_encode_packed()) Ok(args.abi_encode_packed())
} }
@@ -1545,6 +1546,7 @@ mod tests {
#[test] #[test]
fn test_encode_maverick_v2() { fn test_encode_maverick_v2() {
// GHO -> (maverick) -> USDC
let maverick_pool = ProtocolComponent { let maverick_pool = ProtocolComponent {
id: String::from("0x14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67"), id: String::from("0x14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67"),
protocol_system: String::from("vm:maverick_v2"), protocol_system: String::from("vm:maverick_v2"),
@@ -1565,6 +1567,7 @@ mod tests {
router_address: Some(Bytes::default()), router_address: Some(Bytes::default()),
group_token_in: token_in.clone(), group_token_in: token_in.clone(),
group_token_out: token_out.clone(), group_token_out: token_out.clone(),
transfer_type: TransferType::TransferToProtocol,
}; };
let encoder = MaverickV2SwapEncoder::new( let encoder = MaverickV2SwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
@@ -1587,6 +1590,8 @@ mod tests {
"14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67", "14Cf6D2Fe3E1B326114b07d22A6F6bb59e346c67",
// receiver // receiver
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
// transfer from router to protocol
"00",
)) ))
.to_lowercase() .to_lowercase()
); );