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
This commit is contained in:
TAMARA LIPOWSKI
2025-04-14 17:15:19 -04:00
committed by Diana Carvalho
parent 462be5463b
commit 3a73fef933
6 changed files with 66 additions and 23 deletions

View File

@@ -10,14 +10,17 @@ import {
import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol"; import {IAsset} from "@balancer-labs/v2-interfaces/contracts/vault/IAsset.sol";
// slither-disable-next-line solc-version // slither-disable-next-line solc-version
import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol"; import {IVault} from "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import {TokenTransfer} from "./TokenTransfer.sol";
error BalancerV2Executor__InvalidDataLength(); error BalancerV2Executor__InvalidDataLength();
contract BalancerV2Executor is IExecutor { contract BalancerV2Executor is IExecutor, TokenTransfer {
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; address private constant VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
constructor(address _permit2) TokenTransfer(_permit2) {}
// slither-disable-next-line locked-ether // slither-disable-next-line locked-ether
function swap(uint256 givenAmount, bytes calldata data) function swap(uint256 givenAmount, bytes calldata data)
external external
@@ -29,9 +32,20 @@ contract BalancerV2Executor is IExecutor {
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TransferType transferType
) = _decodeData(data); ) = _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) { if (needsApproval) {
// slither-disable-next-line unused-return // slither-disable-next-line unused-return
tokenIn.approve(VAULT, type(uint256).max); tokenIn.approve(VAULT, type(uint256).max);
@@ -67,10 +81,11 @@ contract BalancerV2Executor is IExecutor {
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TransferType transferType
) )
{ {
if (data.length != 93) { if (data.length != 94) {
revert BalancerV2Executor__InvalidDataLength(); revert BalancerV2Executor__InvalidDataLength();
} }
@@ -79,5 +94,6 @@ contract BalancerV2Executor is IExecutor {
poolId = bytes32(data[40:72]); poolId = bytes32(data[40:72]);
receiver = address(bytes20(data[72:92])); receiver = address(bytes20(data[72:92]));
needsApproval = uint8(data[92]) > 0; needsApproval = uint8(data[92]) > 0;
transferType = TransferType(uint8(data[93]));
} }
} }

View File

@@ -64,7 +64,15 @@ contract CurveExecutor is IExecutor, TokenTransfer {
TransferType transferType TransferType transferType
) = _decodeData(data); ) = _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) { if (tokenApprovalNeeded && tokenIn != nativeToken) {
// slither-disable-next-line unused-return // slither-disable-next-line unused-return

View File

@@ -53,7 +53,9 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
_transfer( _transfer(
tokenIn, tokenIn,
msg.sender, 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, amountIn,
transferType transferType
); );

View File

@@ -106,7 +106,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper {
pancakev3Executor = new UniswapV3Executor( pancakev3Executor = new UniswapV3Executor(
factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS
); );
balancerv2Executor = new BalancerV2Executor(); balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS);
ekuboExecutor = new EkuboExecutor(ekuboCore); ekuboExecutor = new EkuboExecutor(ekuboCore);
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS); curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);

View File

@@ -6,6 +6,8 @@ import {Test} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "../Constants.sol"; import {Constants} from "../Constants.sol";
contract BalancerV2ExecutorExposed is BalancerV2Executor { contract BalancerV2ExecutorExposed is BalancerV2Executor {
constructor(address _permit2) BalancerV2Executor(_permit2) {}
function decodeParams(bytes calldata data) function decodeParams(bytes calldata data)
external external
pure pure
@@ -14,18 +16,15 @@ contract BalancerV2ExecutorExposed is BalancerV2Executor {
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TransferType transferType
) )
{ {
return _decodeData(data); return _decodeData(data);
} }
} }
contract BalancerV2ExecutorTest is contract BalancerV2ExecutorTest is Test, Constants {
BalancerV2ExecutorExposed,
Test,
Constants
{
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
BalancerV2ExecutorExposed balancerV2Exposed; BalancerV2ExecutorExposed balancerV2Exposed;
@@ -37,12 +36,17 @@ contract BalancerV2ExecutorTest is
function setUp() public { function setUp() public {
uint256 forkBlock = 17323404; uint256 forkBlock = 17323404;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
balancerV2Exposed = new BalancerV2ExecutorExposed(); balancerV2Exposed = new BalancerV2ExecutorExposed(PERMIT2_ADDRESS);
} }
function testDecodeParams() public view { function testDecodeParams() public view {
bytes memory params = abi.encodePacked( 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, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TokenTransfer.TransferType transferType
) = balancerV2Exposed.decodeParams(params); ) = balancerV2Exposed.decodeParams(params);
assertEq(address(tokenIn), WETH_ADDR); assertEq(address(tokenIn), WETH_ADDR);
@@ -58,6 +63,7 @@ contract BalancerV2ExecutorTest is
assertEq(poolId, WETH_BAL_POOL_ID); assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, address(2)); assertEq(receiver, address(2));
assertEq(needsApproval, true); assertEq(needsApproval, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
} }
function testDecodeParamsInvalidDataLength() public { function testDecodeParamsInvalidDataLength() public {
@@ -70,8 +76,14 @@ contract BalancerV2ExecutorTest is
function testSwap() public { function testSwap() public {
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
bytes memory protocolData = bytes memory protocolData = abi.encodePacked(
abi.encodePacked(WETH_ADDR, BAL_ADDR, WETH_BAL_POOL_ID, BOB, true); WETH_ADDR,
BAL_ADDR,
WETH_BAL_POOL_ID,
BOB,
true,
TokenTransfer.TransferType.NONE
);
deal(WETH_ADDR, address(balancerV2Exposed), amountIn); deal(WETH_ADDR, address(balancerV2Exposed), amountIn);
uint256 balanceBefore = BAL.balanceOf(BOB); uint256 balanceBefore = BAL.balanceOf(BOB);
@@ -86,14 +98,15 @@ contract BalancerV2ExecutorTest is
function testDecodeIntegration() public view { function testDecodeIntegration() public view {
// Generated by the SwapEncoder - test_encode_balancer_v2 // Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData = bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105";
( (
IERC20 tokenIn, IERC20 tokenIn,
IERC20 tokenOut, IERC20 tokenOut,
bytes32 poolId, bytes32 poolId,
address receiver, address receiver,
bool needsApproval bool needsApproval,
TokenTransfer.TransferType transferType
) = balancerV2Exposed.decodeParams(protocolData); ) = balancerV2Exposed.decodeParams(protocolData);
assertEq(address(tokenIn), WETH_ADDR); assertEq(address(tokenIn), WETH_ADDR);
@@ -101,12 +114,13 @@ contract BalancerV2ExecutorTest is
assertEq(poolId, WETH_BAL_POOL_ID); assertEq(poolId, WETH_BAL_POOL_ID);
assertEq(receiver, BOB); assertEq(receiver, BOB);
assertEq(needsApproval, true); assertEq(needsApproval, true);
assertEq(uint8(transferType), uint8(TokenTransfer.TransferType.NONE));
} }
function testSwapIntegration() public { function testSwapIntegration() public {
// Generated by the SwapEncoder - test_encode_balancer_v2 // Generated by the SwapEncoder - test_encode_balancer_v2
bytes memory protocolData = bytes memory protocolData =
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e01"; hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2ba100000625a3754423978a60c9317c58a424e3d5c6ee304399dbdb9c8ef030ab642b10820db8f560002000000000000000000141d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0105";
uint256 amountIn = 10 ** 18; uint256 amountIn = 10 ** 18;
deal(WETH_ADDR, address(balancerV2Exposed), amountIn); deal(WETH_ADDR, address(balancerV2Exposed), amountIn);

View File

@@ -281,6 +281,7 @@ impl SwapEncoder for BalancerV2SwapEncoder {
component_id, component_id,
bytes_to_address(&encoding_context.receiver)?, bytes_to_address(&encoding_context.receiver)?,
approval_needed, approval_needed,
(encoding_context.transfer_type as u8).to_be_bytes(),
); );
Ok(args.abi_encode_packed()) Ok(args.abi_encode_packed())
} }
@@ -703,7 +704,7 @@ mod tests {
router_address: Some(Bytes::zero(20)), router_address: Some(Bytes::zero(20)),
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::Transfer, transfer_type: TransferType::None,
}; };
let encoder = BalancerV2SwapEncoder::new( let encoder = BalancerV2SwapEncoder::new(
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
@@ -731,7 +732,9 @@ mod tests {
// receiver // receiver
"1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e", "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
// approval needed // approval needed
"01" "01",
// transfer type
"05"
)) ))
); );
} }