From 3a73fef9334f3da49247f666866e5b76d44c40dd Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 14 Apr 2025 17:15:19 -0400 Subject: [PATCH] feat: Add TokenTransfer class to BalancerV2 - This needed to be in BalancerV2 so that the executor can also take care of transfers from the user into the tycho router, to avoid having transfer actions mixed between the router and executors --- foundry/src/executors/BalancerV2Executor.sol | 24 +++++++++-- foundry/src/executors/CurveExecutor.sol | 10 ++++- foundry/src/executors/UniswapV4Executor.sol | 4 +- foundry/test/TychoRouterTestSetup.sol | 2 +- .../test/executors/BalancerV2Executor.t.sol | 42 ++++++++++++------- .../evm/swap_encoder/swap_encoders.rs | 7 +++- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/foundry/src/executors/BalancerV2Executor.sol b/foundry/src/executors/BalancerV2Executor.sol index 0f69fcd..817c98b 100644 --- a/foundry/src/executors/BalancerV2Executor.sol +++ b/foundry/src/executors/BalancerV2Executor.sol @@ -10,14 +10,17 @@ import { import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol"; // slither-disable-next-line solc-version import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; +import {TokenTransfer} from "./TokenTransfer.sol"; error BalancerV2Executor__InvalidDataLength(); -contract BalancerV2Executor is IExecutor { +contract BalancerV2Executor is IExecutor, TokenTransfer { using SafeERC20 for IERC20; address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; + constructor(address _permit2) TokenTransfer(_permit2) {} + // slither-disable-next-line locked-ether function swap(uint256 givenAmount, bytes calldata data) external @@ -29,9 +32,20 @@ contract BalancerV2Executor is IExecutor { IERC20 tokenOut, bytes32 poolId, address receiver, - bool needsApproval + bool needsApproval, + TransferType transferType ) = _decodeData(data); + _transfer( + address(tokenIn), + msg.sender, + // Receiver can never be the pool, since the pool expects funds in the router contract + // Thus, this call will only ever be used to transfer funds from the user into the router. + address(this), + givenAmount, + transferType + ); + if (needsApproval) { // slither-disable-next-line unused-return tokenIn.approve(VAULT, type(uint256).max); @@ -67,10 +81,11 @@ contract BalancerV2Executor is IExecutor { IERC20 tokenOut, bytes32 poolId, address receiver, - bool needsApproval + bool needsApproval, + TransferType transferType ) { - if (data.length != 93) { + if (data.length != 94) { revert BalancerV2Executor__InvalidDataLength(); } @@ -79,5 +94,6 @@ contract BalancerV2Executor is IExecutor { poolId = bytes32(data[40:72]); receiver = address(bytes20(data[72:92])); needsApproval = uint8(data[92]) > 0; + transferType = TransferType(uint8(data[93])); } } diff --git a/foundry/src/executors/CurveExecutor.sol b/foundry/src/executors/CurveExecutor.sol index 9a8c1e6..38173e9 100644 --- a/foundry/src/executors/CurveExecutor.sol +++ b/foundry/src/executors/CurveExecutor.sol @@ -64,7 +64,15 @@ contract CurveExecutor is IExecutor, TokenTransfer { TransferType transferType ) = _decodeData(data); - _transfer(tokenIn, msg.sender, pool, amountIn, transferType); + _transfer( + tokenIn, + msg.sender, + // Receiver can never be the pool, since the pool expects funds in the router contract + // Thus, this call will only ever be used to transfer funds from the user into the router. + address(this), + amountIn, + transferType + ); if (tokenApprovalNeeded && tokenIn != nativeToken) { // slither-disable-next-line unused-return diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index adcea98..aafbd7d 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -53,7 +53,9 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer { _transfer( tokenIn, msg.sender, - address(this), // irrelevant attribute + // Receiver can never be the pool, since the pool expects funds in the router contract + // Thus, this call will only ever be used to transfer funds from the user into the router. + address(this), amountIn, transferType ); diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index d5b9651..83b665f 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -106,7 +106,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper { pancakev3Executor = new UniswapV3Executor( factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS ); - balancerv2Executor = new BalancerV2Executor(); + balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS); ekuboExecutor = new EkuboExecutor(ekuboCore); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index 9187187..78b435b 100644 --- a/foundry/test/executors/BalancerV2Executor.t.sol +++ b/foundry/test/executors/BalancerV2Executor.t.sol @@ -6,6 +6,8 @@ import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; contract BalancerV2ExecutorExposed is BalancerV2Executor { + constructor(address _permit2) BalancerV2Executor(_permit2) {} + function decodeParams(bytes calldata data) external pure @@ -14,18 +16,15 @@ contract BalancerV2ExecutorExposed is BalancerV2Executor { IERC20 tokenOut, bytes32 poolId, address receiver, - bool needsApproval + bool needsApproval, + TransferType transferType ) { return _decodeData(data); } } -contract BalancerV2ExecutorTest is - BalancerV2ExecutorExposed, - Test, - Constants -{ +contract BalancerV2ExecutorTest is Test, Constants { using SafeERC20 for IERC20; BalancerV2ExecutorExposed balancerV2Exposed; @@ -37,12 +36,17 @@ contract BalancerV2ExecutorTest is function setUp() public { uint256 forkBlock = 17323404; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - balancerV2Exposed = new BalancerV2ExecutorExposed(); + balancerV2Exposed = new BalancerV2ExecutorExposed(PERMIT2_ADDRESS); } function testDecodeParams() public view { bytes memory params = abi.encodePacked( - WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, address(2), true + WETH_ADDR, + BAL_ADDR, + WETH_BAL_POOL_ID, + address(2), + true, + TokenTransfer.TransferType.NONE ); ( @@ -50,7 +54,8 @@ contract BalancerV2ExecutorTest is IERC20 tokenOut, bytes32 poolId, address receiver, - bool needsApproval + bool needsApproval, + TokenTransfer.TransferType transferType ) = balancerV2Exposed.decodeParams(params); assertEq(address(tokenIn), WETH_ADDR); @@ -58,6 +63,7 @@ contract BalancerV2ExecutorTest is assertEq(poolId, WETH_BAL_POOL_ID); assertEq(receiver, address(2)); assertEq(needsApproval, true); + assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE)); } function testDecodeParamsInvalidDataLength() public { @@ -70,8 +76,14 @@ contract BalancerV2ExecutorTest is function testSwap() public { uint256 amountIn = 10 ** 18; - bytes memory protocolData = - abi.encodePacked(WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, BOB, true); + bytes memory protocolData = abi.encodePacked( + WETH_ADDR, + BAL_ADDR, + WETH_BAL_POOL_ID, + BOB, + true, + TokenTransfer.TransferType.NONE + ); deal(WETH_ADDR, address(balancerV2Exposed), amountIn); uint256 balanceBefore = BAL.balanceOf(BOB); @@ -86,14 +98,15 @@ contract BalancerV2ExecutorTest is function testDecodeIntegration() public view { // Generated by the SwapEncoder - test_encode_balancer_v2 bytes memory protocolData = - hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; + hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105"; ( IERC20 tokenIn, IERC20 tokenOut, bytes32 poolId, address receiver, - bool needsApproval + bool needsApproval, + TokenTransfer.TransferType transferType ) = balancerV2Exposed.decodeParams(protocolData); assertEq(address(tokenIn), WETH_ADDR); @@ -101,12 +114,13 @@ contract BalancerV2ExecutorTest is assertEq(poolId, WETH_BAL_POOL_ID); assertEq(receiver, BOB); assertEq(needsApproval, true); + assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE)); } function testSwapIntegration() public { // Generated by the SwapEncoder - test_encode_balancer_v2 bytes memory protocolData = - hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; + hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105"; uint256 amountIn = 10 ** 18; deal(WETH_ADDR, address(balancerV2Exposed), amountIn); diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 2ea8954..1602ae3 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -281,6 +281,7 @@ impl SwapEncoder for BalancerV2SwapEncoder { component_id, bytes_to_address(&encoding_context.receiver)?, approval_needed, + (encoding_context.transfer_type as u8).to_be_bytes(), ); Ok(args.abi_encode_packed()) } @@ -703,7 +704,7 @@ mod tests { router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), - transfer_type: TransferType::Transfer, + transfer_type: TransferType::None, }; let encoder = BalancerV2SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), @@ -731,7 +732,9 @@ mod tests { // receiver "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", // approval needed - "01" + "01", + // transfer type + "05" )) ); }