From f34db52f09ccbd85213cb08e65cedac56a2b5cf3 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Wed, 4 Jun 2025 17:18:34 +0100 Subject: [PATCH 1/3] feat: Implement BalancerV3Executor Took 2 hours 32 minutes Took 8 seconds Took 13 minutes --- .gitmodules | 3 + foundry/remappings.txt | 1 + foundry/src/executors/BalancerV3Executor.sol | 142 ++++++++++++++++++ .../test/executors/BalancerV2Executor.t.sol | 2 +- .../test/executors/BalancerV3Executor.t.sol | 100 ++++++++++++ 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 foundry/src/executors/BalancerV3Executor.sol create mode 100644 foundry/test/executors/BalancerV3Executor.t.sol diff --git a/.gitmodules b/.gitmodules index d9919b8..f87cde7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "foundry/lib/solady"] path = foundry/lib/solady url = https://github.com/vectorized/solady +[submodule "lib/balancer-v3-monorepo"] + path = lib/balancer-v3-monorepo + url = https://github.com/balancer/balancer-v3-monorepo diff --git a/foundry/remappings.txt b/foundry/remappings.txt index d10f28f..6e43320 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -4,6 +4,7 @@ @src/=src/ @uniswap-v2/=lib/v2-core/ @balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces +@balancer-labs/v3-interfaces=lib/balancer-v3-monorepo/pkg/interfaces @uniswap/v3-updated/=lib/v3-updated/ @uniswap/v3-core/=lib/v3-core/ @uniswap/v4-core/=lib/v4-core/ diff --git a/foundry/src/executors/BalancerV3Executor.sol b/foundry/src/executors/BalancerV3Executor.sol new file mode 100644 index 0000000..8083ca5 --- /dev/null +++ b/foundry/src/executors/BalancerV3Executor.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "@interfaces/IExecutor.sol"; +import { + IERC20, + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { + SwapKind, + VaultSwapParams +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {RestrictTransferFrom} from "../RestrictTransferFrom.sol"; + +error BalancerV3Executor__InvalidDataLength(); +error BalancerV3Executor__SenderIsNotVault(address sender); + +contract BalancerV3Executor is IExecutor, RestrictTransferFrom { + using SafeERC20 for IERC20; + + IVault private constant VAULT = + IVault(0xbA1333333333a1BA1108E8412f11850A5C319bA9); + + /** + * @notice Data for the swap hook. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param amountGiven Amount given based on kind of the swap + * @param transferType Type of transfer to be used for the token in + * @param receiver Address to receive the output token + */ + struct SwapHookParams { + address pool; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountGiven; + TransferType transferType; + address receiver; + } + + constructor(address _permit2) RestrictTransferFrom(_permit2) {} + + modifier onlyVault() { + if (msg.sender != address(VAULT)) { + revert BalancerV3Executor__SenderIsNotVault(msg.sender); + } + _; + } + + // slither-disable-next-line locked-ether + function swap(uint256 givenAmount, bytes calldata data) + external + payable + returns (uint256 calculatedAmount) + { + ( + IERC20 tokenIn, + IERC20 tokenOut, + address poolId, + TransferType transferType, + address receiver + ) = _decodeData(data); + + calculatedAmount = abi.decode( + VAULT.unlock( + abi.encodeCall( + BalancerV3Executor.swapHook, + SwapHookParams({ + pool: poolId, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: givenAmount, + transferType: transferType, + receiver: receiver + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook to be called by the Balancer Vault. + * @param params Parameters for the swap hook. + * @return amountCalculated The amount calculated after the swap. + */ + function swapHook(SwapHookParams calldata params) + external + onlyVault + returns (uint256 amountCalculated) + { + uint256 amountIn; + uint256 amountOut; + (amountCalculated, amountIn, amountOut) = VAULT.swap( + VaultSwapParams({ + kind: SwapKind.EXACT_IN, + pool: params.pool, + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountGivenRaw: params.amountGiven, + limitRaw: 0, + userData: "" + }) + ); + + _transfer( + address(VAULT), + params.transferType, + address(params.tokenIn), + amountIn + ); + // slither-disable-next-line unused-return + VAULT.settle(params.tokenIn, amountIn); + VAULT.sendTo(params.tokenOut, params.receiver, amountOut); + + return amountCalculated; + } + + function _decodeData(bytes calldata data) + internal + pure + returns ( + IERC20 tokenIn, + IERC20 tokenOut, + address poolId, + TransferType transferType, + address receiver + ) + { + if (data.length != 81) { + revert BalancerV3Executor__InvalidDataLength(); + } + + tokenIn = IERC20(address(bytes20(data[0:20]))); + tokenOut = IERC20(address(bytes20(data[20:40]))); + poolId = address(bytes20(data[40:60])); + transferType = TransferType(uint8(data[60])); + receiver = address(bytes20(data[61:81])); + } +} diff --git a/foundry/test/executors/BalancerV2Executor.t.sol b/foundry/test/executors/BalancerV2Executor.t.sol index 6b099f3..6c832d6 100644 --- a/foundry/test/executors/BalancerV2Executor.t.sol +++ b/foundry/test/executors/BalancerV2Executor.t.sol @@ -84,7 +84,7 @@ contract BalancerV2ExecutorTest is Constants, TestUtils { WETH_BAL_POOL_ID, BOB, true, - RestrictTransferFrom.TransferType.Transfer + RestrictTransferFrom.TransferType.None ); deal(WETH_ADDR, address(balancerV2Exposed), amountIn); diff --git a/foundry/test/executors/BalancerV3Executor.t.sol b/foundry/test/executors/BalancerV3Executor.t.sol new file mode 100644 index 0000000..0114863 --- /dev/null +++ b/foundry/test/executors/BalancerV3Executor.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import "../../src/executors/BalancerV3Executor.sol"; +import "../TestUtils.sol"; +import "@src/executors/BalancerV3Executor.sol"; +import {Constants} from "../Constants.sol"; + +contract BalancerV3ExecutorExposed is BalancerV3Executor { + constructor(address _permit2) BalancerV3Executor(_permit2) {} + + function decodeParams(bytes calldata data) + external + pure + returns ( + IERC20 tokenIn, + IERC20 tokenOut, + address poolId, + TransferType transferType, + address receiver + ) + { + return _decodeData(data); + } +} + +contract BalancerV3ExecutorTest is Constants, TestUtils { + using SafeERC20 for IERC20; + + BalancerV3ExecutorExposed balancerV3Exposed; + address WETH_osETH_pool = + address(0x57c23c58B1D8C3292c15BEcF07c62C5c52457A42); + address osETH_ADDR = address(0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38); + address waEthWETH_ADDR = address(0x0bfc9d54Fc184518A81162F8fB99c2eACa081202); + + function setUp() public { + uint256 forkBlock = 22625131; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + balancerV3Exposed = new BalancerV3ExecutorExposed(PERMIT2_ADDRESS); + } + + function testDecodeParams() public view { + bytes memory params = abi.encodePacked( + osETH_ADDR, + waEthWETH_ADDR, + WETH_osETH_pool, + RestrictTransferFrom.TransferType.None, + BOB + ); + + ( + IERC20 tokenIn, + IERC20 tokenOut, + address poolId, + RestrictTransferFrom.TransferType transferType, + address receiver + ) = balancerV3Exposed.decodeParams(params); + + assertEq(address(tokenIn), osETH_ADDR); + assertEq(address(tokenOut), waEthWETH_ADDR); + assertEq(poolId, WETH_osETH_pool); + assertEq( + uint8(transferType), uint8(RestrictTransferFrom.TransferType.None) + ); + assertEq(receiver, BOB); + } + + function testDecodeParamsInvalidDataLength() public { + bytes memory invalidParams = abi.encodePacked( + osETH_ADDR, + waEthWETH_ADDR, + WETH_osETH_pool, + RestrictTransferFrom.TransferType.None + ); + + vm.expectRevert(BalancerV3Executor__InvalidDataLength.selector); + balancerV3Exposed.decodeParams(invalidParams); + } + + function testSwap() public { + uint256 amountIn = 10 ** 18; + bytes memory protocolData = abi.encodePacked( + osETH_ADDR, + waEthWETH_ADDR, + WETH_osETH_pool, + RestrictTransferFrom.TransferType.Transfer, + BOB + ); + + deal(osETH_ADDR, address(balancerV3Exposed), amountIn); + + uint256 balanceBefore = IERC20(waEthWETH_ADDR).balanceOf(BOB); + + uint256 amountOut = balancerV3Exposed.swap(amountIn, protocolData); + + uint256 balanceAfter = IERC20(waEthWETH_ADDR).balanceOf(BOB); + assertGt(balanceAfter, balanceBefore); + assertEq(balanceAfter - balanceBefore, amountOut); + } +} From f173f9bed7e942102baba48e89b262206b7260fd Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Wed, 4 Jun 2025 17:55:19 +0100 Subject: [PATCH 2/3] feat: Add balancer-v3-monorepo lib Took 6 minutes --- .gitmodules | 4 ++-- foundry/lib/balancer-v3-monorepo | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 160000 foundry/lib/balancer-v3-monorepo diff --git a/.gitmodules b/.gitmodules index f87cde7..025aab1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,6 @@ [submodule "foundry/lib/solady"] path = foundry/lib/solady url = https://github.com/vectorized/solady -[submodule "lib/balancer-v3-monorepo"] - path = lib/balancer-v3-monorepo +[submodule "foundry/lib/balancer-v3-monorepo"] + path = foundry/lib/balancer-v3-monorepo url = https://github.com/balancer/balancer-v3-monorepo diff --git a/foundry/lib/balancer-v3-monorepo b/foundry/lib/balancer-v3-monorepo new file mode 160000 index 0000000..84cfb0d --- /dev/null +++ b/foundry/lib/balancer-v3-monorepo @@ -0,0 +1 @@ +Subproject commit 84cfb0d9d0bd024f92c7a95c64ab7ab8b4c12f4b From 806ce4e62cfb80c16e222358a9a04b6dd5ff5bc1 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 5 Jun 2025 09:58:31 +0100 Subject: [PATCH 3/3] feat: Make BalancerV3Executor inherit from ICallback Took 37 minutes --- foundry/src/executors/BalancerV3Executor.sol | 114 ++++++------------ .../test/executors/BalancerV3Executor.t.sol | 4 + 2 files changed, 44 insertions(+), 74 deletions(-) diff --git a/foundry/src/executors/BalancerV3Executor.sol b/foundry/src/executors/BalancerV3Executor.sol index 8083ca5..3c09c46 100644 --- a/foundry/src/executors/BalancerV3Executor.sol +++ b/foundry/src/executors/BalancerV3Executor.sol @@ -12,50 +12,47 @@ import { VaultSwapParams } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; import {RestrictTransferFrom} from "../RestrictTransferFrom.sol"; +import {ICallback} from "../../interfaces/ICallback.sol"; error BalancerV3Executor__InvalidDataLength(); error BalancerV3Executor__SenderIsNotVault(address sender); -contract BalancerV3Executor is IExecutor, RestrictTransferFrom { +contract BalancerV3Executor is IExecutor, RestrictTransferFrom, ICallback { using SafeERC20 for IERC20; IVault private constant VAULT = IVault(0xbA1333333333a1BA1108E8412f11850A5C319bA9); - /** - * @notice Data for the swap hook. - * @param pool Address of the liquidity pool - * @param tokenIn Token to be swapped from - * @param tokenOut Token to be swapped to - * @param amountGiven Amount given based on kind of the swap - * @param transferType Type of transfer to be used for the token in - * @param receiver Address to receive the output token - */ - struct SwapHookParams { - address pool; - IERC20 tokenIn; - IERC20 tokenOut; - uint256 amountGiven; - TransferType transferType; - address receiver; - } - constructor(address _permit2) RestrictTransferFrom(_permit2) {} - modifier onlyVault() { - if (msg.sender != address(VAULT)) { - revert BalancerV3Executor__SenderIsNotVault(msg.sender); - } - _; - } - // slither-disable-next-line locked-ether function swap(uint256 givenAmount, bytes calldata data) external payable returns (uint256 calculatedAmount) { + bytes memory result = VAULT.unlock( + abi.encodeCall( + BalancerV3Executor.handleCallback, + abi.encodePacked(givenAmount, data) + ) + ); + calculatedAmount = abi.decode(abi.decode(result, (bytes)), (uint256)); + } + + function verifyCallback(bytes calldata /*data*/ ) public view { + if (msg.sender != address(VAULT)) { + revert BalancerV3Executor__SenderIsNotVault(msg.sender); + } + } + + function handleCallback(bytes calldata data) + external + returns (bytes memory result) + { + verifyCallback(data); ( + uint256 amountGiven, IERC20 tokenIn, IERC20 tokenOut, address poolId, @@ -63,65 +60,33 @@ contract BalancerV3Executor is IExecutor, RestrictTransferFrom { address receiver ) = _decodeData(data); - calculatedAmount = abi.decode( - VAULT.unlock( - abi.encodeCall( - BalancerV3Executor.swapHook, - SwapHookParams({ - pool: poolId, - tokenIn: tokenIn, - tokenOut: tokenOut, - amountGiven: givenAmount, - transferType: transferType, - receiver: receiver - }) - ) - ), - (uint256) - ); - } - - /** - * @notice Hook to be called by the Balancer Vault. - * @param params Parameters for the swap hook. - * @return amountCalculated The amount calculated after the swap. - */ - function swapHook(SwapHookParams calldata params) - external - onlyVault - returns (uint256 amountCalculated) - { + uint256 amountCalculated; uint256 amountIn; uint256 amountOut; (amountCalculated, amountIn, amountOut) = VAULT.swap( VaultSwapParams({ kind: SwapKind.EXACT_IN, - pool: params.pool, - tokenIn: params.tokenIn, - tokenOut: params.tokenOut, - amountGivenRaw: params.amountGiven, + pool: poolId, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGivenRaw: amountGiven, limitRaw: 0, userData: "" }) ); - _transfer( - address(VAULT), - params.transferType, - address(params.tokenIn), - amountIn - ); + _transfer(address(VAULT), transferType, address(tokenIn), amountIn); // slither-disable-next-line unused-return - VAULT.settle(params.tokenIn, amountIn); - VAULT.sendTo(params.tokenOut, params.receiver, amountOut); - - return amountCalculated; + VAULT.settle(tokenIn, amountIn); + VAULT.sendTo(tokenOut, receiver, amountOut); + return abi.encode(amountCalculated); } function _decodeData(bytes calldata data) internal pure returns ( + uint256 amountGiven, IERC20 tokenIn, IERC20 tokenOut, address poolId, @@ -129,14 +94,15 @@ contract BalancerV3Executor is IExecutor, RestrictTransferFrom { address receiver ) { - if (data.length != 81) { + if (data.length != 113) { revert BalancerV3Executor__InvalidDataLength(); } - tokenIn = IERC20(address(bytes20(data[0:20]))); - tokenOut = IERC20(address(bytes20(data[20:40]))); - poolId = address(bytes20(data[40:60])); - transferType = TransferType(uint8(data[60])); - receiver = address(bytes20(data[61:81])); + amountGiven = uint256(bytes32(data[0:32])); + tokenIn = IERC20(address(bytes20(data[32:52]))); + tokenOut = IERC20(address(bytes20(data[52:72]))); + poolId = address(bytes20(data[72:92])); + transferType = TransferType(uint8(data[92])); + receiver = address(bytes20(data[93:113])); } } diff --git a/foundry/test/executors/BalancerV3Executor.t.sol b/foundry/test/executors/BalancerV3Executor.t.sol index 0114863..51b2e74 100644 --- a/foundry/test/executors/BalancerV3Executor.t.sol +++ b/foundry/test/executors/BalancerV3Executor.t.sol @@ -13,6 +13,7 @@ contract BalancerV3ExecutorExposed is BalancerV3Executor { external pure returns ( + uint256 amountGiven, IERC20 tokenIn, IERC20 tokenOut, address poolId, @@ -41,6 +42,7 @@ contract BalancerV3ExecutorTest is Constants, TestUtils { function testDecodeParams() public view { bytes memory params = abi.encodePacked( + uint256(1 ether), osETH_ADDR, waEthWETH_ADDR, WETH_osETH_pool, @@ -49,6 +51,7 @@ contract BalancerV3ExecutorTest is Constants, TestUtils { ); ( + uint256 amountGiven, IERC20 tokenIn, IERC20 tokenOut, address poolId, @@ -56,6 +59,7 @@ contract BalancerV3ExecutorTest is Constants, TestUtils { address receiver ) = balancerV3Exposed.decodeParams(params); + assertEq(amountGiven, 1 ether); assertEq(address(tokenIn), osETH_ADDR); assertEq(address(tokenOut), waEthWETH_ADDR); assertEq(poolId, WETH_osETH_pool);