diff --git a/packages/foundry/contracts/hooks/OrderHook.sol b/packages/foundry/contracts/hooks/OrderHook.sol new file mode 100644 index 00000000..b9a1938b --- /dev/null +++ b/packages/foundry/contracts/hooks/OrderHook.sol @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.24; + +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import {IBasePoolFactory} from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; +import {LiquidityManagement, TokenConfig, HookFlags, AfterSwapParams} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {VaultGuard} from "@balancer-labs/v3-vault/contracts/VaultGuard.sol"; +import {BaseHooks} from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; + +/// @title A Hook to trade coins in a balancer v3 pool +/// @author Atiq Ishrak +/// @notice This hook allows users to place orders in a pool and execute them based on the current price of the pool +contract OrderHook is BaseHooks, VaultGuard, ReentrancyGuard { + using SafeERC20 for IERC20; + + /*///////////////////////////// + /////// ERRORS /////// + /////////////////////////////*/ + + error OrderHook__UnauthorizedRouter(); + error OrderHook__OrderAmountInMustBeMoreThanZero(); + error OrderHook__InsufficientAllowance(); + error OrderHook__InsufficientBalance(); + error OrderHook__UnauthorizedCaller(); + error OrderHook__OrderAlreadyExecutedOrCancelled(); + error OrderHook__OrderUnableToExecute( + bytes32 orderId, + address user, + uint256 currentTokenInPrice, + uint256 triggerPrice, + OrderType orderType + ); + + /*///////////////////////////// + /////// EVENTS /////// + /////////////////////////////*/ + + event OrderPlaced( + bytes32 indexed orderId, + address indexed user, + OrderType orderType, + uint256 amountIn, + uint256 triggerPrice, + uint8 slippageTolerance + ); + event OrderExecuted(bytes32 indexed orderId, address indexed user); + event OrderCanceled( + bytes32 indexed orderId, + address indexed user, + OrderType orderType + ); + event OrdersProcessed(bytes32[] orderIds); + event AfterSwapPrice( + address indexed pool, + address indexed tokenIn, + address indexed tokenOut, + uint256 currentTokenInPrice, + uint256 currentTokenOutPrice + ); + + /*///////////////////////////// + ///// ENUMS, STRUCTS ////// + /////////////////////////////*/ + + enum OrderType { + STOP_LOSS, + BUY_STOP, + BUY_LIMIT, + TAKE_PROFIT + } + + enum OrderStatus { + OPEN, + EXECUTED, + CANCELLED + } + + struct Order { + bytes32 orderId; + address trader; + OrderType orderType; + OrderStatus orderStatus; + uint256 amountIn; + uint256 triggerPrice; + address tokenIn; + address tokenOut; + address pool; + uint8 slippageTolerance; + } + + /*///////////////////////////// + /////// VARIABLES /////// + /////////////////////////////*/ + + // only calls from a trusted routers are allowed to call this hook, because the hook relies on the getSender + // implementation to work properly + address private immutable i_trustedRouter; + // only pools from the allowedFactory are able to register and use this hook + address private immutable i_allowedFactory; + IVault private immutable i_vault; + IPermit2 private immutable i_permit2; + + uint256 public orderCount; + mapping(bytes32 => Order) public orders; + mapping(address => bytes32[]) public userOrders; + // pool address => token address => price + mapping(address => mapping(IERC20 => uint256)) public prevPrices; + + /// @notice Only the trader or the owner of the order is allowed + modifier Onlytrader(address trader, address caller) { + if (trader != caller) { + revert OrderHook__UnauthorizedCaller(); + } + _; + } + + /// @notice Construct a new OrderHook + /// @param vault The vault contract + /// @param allowedFactory The factory that is allowed to use this hook + /// @param trustedRouter The router that is allowed to call this hook + /// @param permit2 Contract to approve tokens + constructor( + IVault vault, + address allowedFactory, + address trustedRouter, + IPermit2 permit2 + ) VaultGuard(vault) { + i_allowedFactory = allowedFactory; + i_trustedRouter = trustedRouter; + i_vault = vault; + i_permit2 = permit2; + } + + // Receive function to receive ether + receive() external payable {} + + // Fallback function to receive ether + fallback() external payable {} + + /// @notice Get the hook flags + function getHookFlags() + public + pure + override + returns (HookFlags memory hookFlags) + { + hookFlags.shouldCallAfterSwap = true; + } + + /// @notice Check if the pool is allowed to register with this hook + /// @dev This hook implements a restrictive approach, where we check if the factory is an allowed factory and if the pool is created by the allowed factory + /// @param factory The factory contract that is deploying the pool + /// @param pool The pool that is being registered with this hook + function onRegister( + address factory, + address pool, + TokenConfig[] memory /* tokenConfig */, + LiquidityManagement calldata /* liquidityManagement */ + ) public view override onlyVault returns (bool) { + // This hook implements a restrictive approach, where we check if the factory is an allowed factory and if + // the pool was created by the allowed factory + return + factory == i_allowedFactory && + IBasePoolFactory(factory).isPoolFromFactory(pool); + } + + /*/////////////////////////////////// + /////////// HOOK /////////// + ///////////////////////////////////*/ + + /// @notice After swap hook + /// @dev This hook is called after a swap is executed. We store the previous prices of the tokens in the pool + /// @dev This emits an event with the current prices so that we can use that offchain to evalaute the positions of the current active orders + /// @dev Only the vault and trusted router is allowed to call this hook + /// @param params The parameters of the swap + function onAfterSwap( + AfterSwapParams calldata params + ) + public + override + onlyVault + returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) + { + if (params.router != i_trustedRouter) { + revert OrderHook__UnauthorizedRouter(); + } + + emit AfterSwapPrice( + params.pool, + address(params.tokenIn), + address(params.tokenOut), + params.tokenInBalanceScaled18, + params.tokenOutBalanceScaled18 + ); + + prevPrices[params.pool][params.tokenIn] = params.tokenInBalanceScaled18; + prevPrices[params.pool][params.tokenOut] = params + .tokenOutBalanceScaled18; + + return (true, params.amountCalculatedRaw); + } + + /*/////////////////////////////////// + /////// PLACE ORDER //////// + ///////////////////////////////////*/ + + /// @notice Place an order in a pool + /// @dev The order is placed by transferring the tokenIn to this contract + /// @param orderType Type of the order + /// @param amountIn Amount of tokenIn + /// @param triggerPrice The Price at which the order should be executed + /// @param pool Pool address + /// @param tokenIn Address of the tokenIn + /// @param tokenOut Address of the tokenOut + /// @param slippageTolerance Slippage percentage allowed + /// @return orderId The id of the order + function placeOrder( + OrderType orderType, + uint256 amountIn, + uint256 triggerPrice, + address pool, + address tokenIn, + address tokenOut, + uint8 slippageTolerance + ) external returns (bytes32 orderId) { + if (amountIn == 0) { + revert OrderHook__OrderAmountInMustBeMoreThanZero(); + } + + i_vault.getPoolTokenCountAndIndexOfToken(pool, IERC20(tokenIn)); + i_vault.getPoolTokenCountAndIndexOfToken(pool, IERC20(tokenOut)); + + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + + orderId = keccak256( + abi.encodePacked(msg.sender, orderCount, block.timestamp) + ); + + orders[orderId] = Order({ + orderId: orderId, + trader: msg.sender, + orderType: orderType, + orderStatus: OrderStatus.OPEN, + amountIn: amountIn, + triggerPrice: triggerPrice, + tokenIn: tokenIn, + tokenOut: tokenOut, + pool: pool, + slippageTolerance: slippageTolerance + }); + + userOrders[msg.sender].push(orderId); + orderCount++; + + emit OrderPlaced( + orderId, + msg.sender, + orderType, + amountIn, + triggerPrice, + slippageTolerance + ); + } + + /*/////////////////////////////////// + /////// CANCEL ORDER //////// + ///////////////////////////////////*/ + + /// @notice Cancel an order + /// @dev The order is cancelled and tokens are transferred back to the trader + /// @param orderId The id of the order + function cancelOrder( + bytes32 orderId + ) external nonReentrant Onlytrader(orders[orderId].trader, msg.sender) { + if (orders[orderId].orderStatus != OrderStatus.OPEN) { + revert OrderHook__OrderAlreadyExecutedOrCancelled(); + } + + orders[orderId].orderStatus = OrderStatus.CANCELLED; + IERC20(orders[orderId].tokenIn).transfer( + msg.sender, + orders[orderId].amountIn + ); + + emit OrderCanceled(orderId, msg.sender, orders[orderId].orderType); + } + + /*/////////////////////////////////// + /////// EXECUTE ORDER //////// + ///////////////////////////////////*/ + + /// @notice Execute an order + /// @dev The order is executed only if the current price of the pool satisfies the order conditions + /// @dev After execution the tokenOut is transferred to the trader + /// @param orderId The id of the order to execute + /// @param data The data to pass to the router + function executeOrder( + bytes32 orderId, + bytes calldata data + ) external nonReentrant Onlytrader(orders[orderId].trader, msg.sender) { + Order storage order = orders[orderId]; + if (order.orderStatus != OrderStatus.OPEN) { + revert OrderHook__OrderAlreadyExecutedOrCancelled(); + } + + uint256[] memory poolPrice = getPoolPrice(order.pool); + + (, uint256 inTokenIndex) = i_vault.getPoolTokenCountAndIndexOfToken( + order.pool, + IERC20(order.tokenIn) + ); + (, uint256 outTokenIndex) = i_vault.getPoolTokenCountAndIndexOfToken( + order.pool, + IERC20(order.tokenOut) + ); + + if (_shouldExecuteOrder(order, poolPrice[inTokenIndex])) { + uint256 limit = _calculateLimit( + order.amountIn, + poolPrice[inTokenIndex], + poolPrice[outTokenIndex], + order.slippageTolerance + ); + + _swap(order, limit, data); + + emit OrderExecuted(orderId, msg.sender); + } else { + revert OrderHook__OrderUnableToExecute( + orderId, + msg.sender, + poolPrice[inTokenIndex], + order.triggerPrice, + order.orderType + ); + } + } + + /*/////////////////////////////////// + ////// INTERNAL FUNCTIONS ////// + ///////////////////////////////////*/ + + /// @notice Swaps tokens in a pool, called by executeOrder + /// @dev The i_permit2 contract is used to approve tokens for the router on behalf of this contract + /// @param order The order to execute + /// @param limit Amount of minimum tokenOut to receive + /// @param data The data to pass to the router + function _swap( + Order storage order, + uint256 limit, + bytes calldata data + ) internal { + IERC20(order.tokenIn).approve(address(i_permit2), order.amountIn); + i_permit2.approve( + order.tokenIn, + address(i_trustedRouter), + uint160(order.amountIn), + type(uint48).max + ); + + uint256 amountOut = IRouter(payable(i_trustedRouter)) + .swapSingleTokenExactIn( + order.pool, + IERC20(order.tokenIn), + IERC20(order.tokenOut), + order.amountIn, + limit, + block.timestamp + 1 hours, + false, + data + ); + + IERC20(order.tokenIn).approve(address(i_permit2), 0); + IERC20(order.tokenOut).safeTransfer(order.trader, amountOut); + order.orderStatus = OrderStatus.EXECUTED; + } + + /// @notice Checks if the order should be executed + /// @dev If the order is a stop loss and the tokenIn price is less than or equal to the trigger price, the order should be executed + /// @dev If the order is a buy stop and the tokenIn price is greater than or equal to the trigger price, the order should be executed + /// @dev If the order is a buy limit and the tokenIn price is less than or equal to the trigger price, the order should be executed + /// @dev If the order is a take profit and the tokenIn price is greater than or equal to the trigger price, the order should be executed + /// @param order The order to check + /// @param tokenInlPrice The price of the tokenIn in the pool where order is placed + function _shouldExecuteOrder( + Order storage order, + uint256 tokenInlPrice + ) internal view returns (bool) { + if ( + order.orderType == OrderType.STOP_LOSS && + tokenInlPrice <= order.triggerPrice + ) { + return true; + } else if ( + order.orderType == OrderType.BUY_STOP && + tokenInlPrice >= order.triggerPrice + ) { + return true; + } else if ( + order.orderType == OrderType.BUY_LIMIT && + tokenInlPrice <= order.triggerPrice + ) { + return true; + } else if ( + order.orderType == OrderType.TAKE_PROFIT && + tokenInlPrice >= order.triggerPrice + ) { + return true; + } + return false; + } + + /// @notice Calculate the limit of the tokenOut for a swap + /// @dev The limit is calculated based on the slippage tolerance + /// @param amountIn Amount of tokenIn + /// @param tokenInPrice Current price of tokenIn + /// @param tokenOutPrice Current price of tokenOut + /// @param slippageTolerance Slippage tolerance + /// @return limit The minimum amount of tokenOut to receive + function _calculateLimit( + uint256 amountIn, + uint256 tokenInPrice, + uint256 tokenOutPrice, + uint8 slippageTolerance + ) internal pure returns (uint256 limit) { + uint256 estimatedAmountOut = (amountIn * tokenOutPrice) / tokenInPrice; + limit = ((estimatedAmountOut * (100 - slippageTolerance)) / 100); + } + + /*//////////////////////////////////////////////// + /////// PUBLIC, VIEW, GETTER FUNCTIONS /////// + ////////////////////////////////////////////////*/ + + /// @notice Get the current prices of the tokens in a pool + /// @param pool The pool address + function getPoolPrice( + address pool + ) public view returns (uint256[] memory balancesLiveScaled18) { + balancesLiveScaled18 = i_vault.getCurrentLiveBalances(pool); + } + + /// @notice Get the tokens in a pool + /// @param pool The pool address + function getPoolTokens( + address pool + ) public view returns (IERC20[] memory tokens) { + tokens = i_vault.getPoolTokens(pool); + } + + /// @notice Get an order by id + /// @param orderId The id of the order + function getOrderById(bytes32 orderId) public view returns (Order memory) { + return orders[orderId]; + } + + /// @notice Get all orders of a user + /// @dev The userOrders mapping stores the order ids of a user + /// @dev We use this mapping to get all the orders of a user + /// @return userOrdersNew An array of orders of the user + function getUserOrders() + public + view + returns (Order[] memory userOrdersNew) + { + bytes32[] memory orderIds = userOrders[msg.sender]; + userOrdersNew = new Order[](orderIds.length); + for (uint256 i = 0; i < userOrdersNew.length; i++) { + userOrdersNew[i] = orders[orderIds[i]]; + } + } +} diff --git a/packages/foundry/contracts/hooks/README.md b/packages/foundry/contracts/hooks/README.md new file mode 100644 index 00000000..00268b66 --- /dev/null +++ b/packages/foundry/contracts/hooks/README.md @@ -0,0 +1,39 @@ +# BAL Hookathon - OrderHoook + +## Overview + +OrderHook is a custom Balancer V3 hook designed to automate and enhance the management of token swaps in decentralized exchanges. This contract enables users to define conditional orders, such as stop-loss, take-profit, buy-stop and stop-limit orders, directly within a Balancer pool. + +The hook is triggered after a swap occurs in the Balancer pool, capturing key data about the transaction (such as token addresses and their current prices). It emits this data as events, which can be processed by off-chain systems or other smart contracts to implement advanced order execution strategies or to notify users about the status of their orders. + +## What the Hook Does + +OrderHook is triggered after a swap operation in a Balancer pool. It is designed to: + +- Emit Events for Post-Swap Processing: The hook captures essential information such as ```tokenIn```, ```tokenOut``` addresses, and their respective amounts swapped. These details are emitted as events, enabling off-chain systems or other smart contracts to react and process the swap results in real-time. + +- Price Tracking and Analytics: The hook records the prices of ```tokenIn``` and ```tokenOut``` within the pool at the time of the swap. This data is emitted as part of the events, which can be used for analytics, on-chain reporting or to dynamically adjust strategies based on price movements. + +- Support for Advanced Order Types: The hook processes four types of orders—Stop Loss, Buy Stop, Stop Limit and Take Profit. These advanced order types enable users to manage risk and automate trading strategies by executing swaps based on specific price thresholds or market conditions. + +- On-Chain Strategy Execution: The hook’s event system can serve as a trigger for executing automated strategies, such as rebalancing portfolios, adjusting liquidity ratios or triggering conditional swaps based on predefined rules. + +- Security and Auditing: By emitting detailed swap event data, the hook provides transparency and traceability for each transaction, which can be valuable for audits, risk management and security analysis. + +## Example Use Case + +Imagine a decentralized trading platform where users can place take-profit orders on token swaps. OrderHook can be integrated into this platform to facilitate order execution as follows: + +- A user wants to make a swap that automatically executes when the price of a specific token reaches a certain threshold, allowing them to secure profits. +- The user submits their order to the OrderHook smart contract, specifying the token pair, desired price threshold, and swap parameters. +- When the token price in the Balancer pool meets or exceeds the specified threshold, OrderHook emits an event containing details such as the prices of tokenIn and tokenOut, their corresponding addresses, and the pool address. +- The platform's backend system or any backend service listens for these events and triggers a notification to the user, informing them that their take-profit conditions have been met. +- Based on this event, the trade is automatically executed to swap the tokens, or in case of slippage, the user can be refunded. + +## Feedback About Developer Experience (DevX) + +Working with the Balancer V3 hook framework has been an exciting experience. The modular design of the system makes it easy to plug in custom logic at various stages of the swap lifecycle. Here are a few insights from the development process: + +- Flexibility: The hook architecture offers tremendous flexibility, allowing for custom workflows and integration with other DeFi systems like Uniswap or cross-chain operations. + +- Documentation and Tooling: While the overall documentation for Balancer V3 was helpful, more examples and tooling specific to hooks would enhance the developer experience. Offering more templates or pre-built hooks could reduce development time and improve onboarding for new developers. diff --git a/packages/foundry/script/orderHook/00-DeployMockTokens.s.sol b/packages/foundry/script/orderHook/00-DeployMockTokens.s.sol new file mode 100755 index 00000000..50619d23 --- /dev/null +++ b/packages/foundry/script/orderHook/00-DeployMockTokens.s.sol @@ -0,0 +1,46 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {MockToken1} from "../src/mocks/MockToken1.sol"; +import {MockToken2} from "../src/mocks/MockToken2.sol"; +import {MockVeBAL} from "../src/mocks/MockVeBAL.sol"; + +import {HelperConfig} from "./HelperConfig.s.sol"; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +/** + * @title Deploy Mock Tokens + * @notice Deploys mock tokens for use with pools and hooks + */ +contract DeployMockTokens is Script { + HelperConfig helperConfig; + + function run() external { + deployMockTokens(); + } + + function deployMockTokens() + public + returns (address mockToken1, address mockToken2, address mockVeBAL) + { + helperConfig = new HelperConfig(); + // Start creating the transactions + address deployer = helperConfig.getConfig().account; + console.log("Deployer: ", deployer); + vm.startBroadcast(deployer); + + // Used to register & initialize pool contracts + mockToken1 = address(new MockToken1("Test Token 1", "AI", 1000e18)); + mockToken2 = address(new MockToken2("Test Token 2", "AIS", 1000e18)); + console.log("MockToken1 deployed at: %s", mockToken1); + console.log("MockToken2 deployed at: %s", mockToken2); + + // Used for the VeBALFeeDiscountHook + mockVeBAL = address(new MockVeBAL("Vote-escrow BAL", "veBAL", 1000e18)); + console.log("Mock Vote-escrow BAL deployed at: %s", mockVeBAL); + + vm.stopBroadcast(); + } +} diff --git a/packages/foundry/script/orderHook/01-DeployConstantSumPool.s.sol b/packages/foundry/script/orderHook/01-DeployConstantSumPool.s.sol new file mode 100755 index 00000000..c8cbaf06 --- /dev/null +++ b/packages/foundry/script/orderHook/01-DeployConstantSumPool.s.sol @@ -0,0 +1,175 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TokenConfig, TokenType, LiquidityManagement, PoolRoleAccounts} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {IRateProvider} from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import {InputHelpers} from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; +import {IVault} from "@balancer-labs/contracts/interfaces/contracts/vault/IVault.sol"; +import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; + +import {Router} from "@balancer-labs/contracts/vault/contracts/Router.sol"; + +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import {ConstantSumFactory} from "../src/factories/ConstantSumFactory.sol"; +import {OrderHook} from "../src/OrderHook.sol"; + +import {Script, console} from "forge-std/Script.sol"; + +import {HelperConfig} from "./HelperConfig.s.sol"; +import {PoolHelpers, CustomPoolConfig, InitializationConfig} from "./PoolHelpers.sol"; + +/** + * @title Deploy Constant Sum Pool + * @notice Deploys, registers, and initializes a constant sum pool that uses a swap fee discount hook + */ +contract DeployConstantSumPool is Script, PoolHelpers { + function deployConstantSumPool( + address token1, + address token2 + ) public returns (address orderHook, address pool) { + HelperConfig helperConfig = new HelperConfig(); + // Set the pool's deployment, registration, and initialization config + CustomPoolConfig memory poolConfig = getSumPoolConfig(token1, token2); + InitializationConfig memory initConfig = getSumPoolInitConfig( + token1, + token2 + ); + + // Start creating the transactions + address deployer = helperConfig.getConfig().account; + vm.startBroadcast(deployer); + + // Deploy a factory + ConstantSumFactory factory = new ConstantSumFactory( + IVault(helperConfig.getConfig().vault), + 0 days + ); // pauseWindowDuration + console.log("Constant Sum Factory deployed at: %s", address(factory)); + + // Deploy a hook + orderHook = address( + new OrderHook( + IVault(helperConfig.getConfig().vault), + address(factory), + helperConfig.getConfig().router, + IPermit2(helperConfig.getConfig().permit2) + ) + ); + console.log("OrderHook deployed at address: %s", orderHook); + + // Deploy a pool and register it with the vault + pool = factory.create( + poolConfig.name, + poolConfig.symbol, + poolConfig.salt, + poolConfig.tokenConfigs, + poolConfig.swapFeePercentage, + poolConfig.protocolFeeExempt, + poolConfig.roleAccounts, + orderHook, // poolHooksContract + poolConfig.liquidityManagement + ); + console.log("Constant Sum Pool deployed at: %s", pool); + + // Approve the router to spend tokens for pool initialization + approveRouterWithPermit2(initConfig.tokens); + + // Seed the pool with initial liquidity + router.initialize( + pool, + initConfig.tokens, + initConfig.exactAmountsIn, + initConfig.minBptAmountOut, + initConfig.wethIsEth, + initConfig.userData + ); + console.log("Constant Sum Pool initialized successfully!"); + vm.stopBroadcast(); + } + + /** + * @dev Set all of the configurations for deploying and registering a pool here + * @notice TokenConfig encapsulates the data required for the Vault to support a token of the given type. + * For STANDARD tokens, the rate provider address must be 0, and paysYieldFees must be false. + * All WITH_RATE tokens need a rate provider, and may or may not be yield-bearing. + */ + function getSumPoolConfig( + address token1, + address token2 + ) internal view returns (CustomPoolConfig memory config) { + string memory name = "Constant Sum Pool"; // name for the pool + string memory symbol = "CSP"; // symbol for the BPT + bytes32 salt = keccak256(abi.encode(block.number)); // salt for the pool deployment via factory + uint256 swapFeePercentage = 0.01e18; // 1% + bool protocolFeeExempt = true; + address poolHooksContract = address(0); // zero address if no hooks contract is needed + + TokenConfig[] memory tokenConfigs = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage. + tokenConfigs[0] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token1), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + tokenConfigs[1] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token2), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + + PoolRoleAccounts memory roleAccounts = PoolRoleAccounts({ + pauseManager: address(0), // Account empowered to pause/unpause the pool (or 0 to delegate to governance) + swapFeeManager: address(0), // Account empowered to set static swap fees for a pool (or 0 to delegate to goverance) + poolCreator: address(0) // Account empowered to set the pool creator fee percentage + }); + LiquidityManagement memory liquidityManagement = LiquidityManagement({ + disableUnbalancedLiquidity: false, + enableAddLiquidityCustom: false, + enableRemoveLiquidityCustom: false, + enableDonation: false + }); + + config = CustomPoolConfig({ + name: name, + symbol: symbol, + salt: salt, + tokenConfigs: sortTokenConfig(tokenConfigs), + swapFeePercentage: swapFeePercentage, + protocolFeeExempt: protocolFeeExempt, + roleAccounts: roleAccounts, + poolHooksContract: poolHooksContract, + liquidityManagement: liquidityManagement + }); + } + + /** + * @dev Set the pool initialization configurations here + * @notice this is where the amounts of tokens to be initially added to the pool are set + */ + function getSumPoolInitConfig( + address token1, + address token2 + ) internal pure returns (InitializationConfig memory config) { + IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool + tokens[0] = IERC20(token1); + tokens[1] = IERC20(token2); + uint256[] memory exactAmountsIn = new uint256[](2); // Exact amounts of tokens to be added, sorted in token alphanumeric order + exactAmountsIn[0] = 50e18; // amount of token1 to send during pool initialization + exactAmountsIn[1] = 50e18; // amount of token2 to send during pool initialization + uint256 minBptAmountOut = 99e18; // Minimum amount of pool tokens to be received + bool wethIsEth = false; // If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens + bytes memory userData = bytes(""); // Additional (optional) data required for adding initial liquidity + + config = InitializationConfig({ + tokens: InputHelpers.sortTokens(tokens), + exactAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + wethIsEth: wethIsEth, + userData: userData + }); + } +} diff --git a/packages/foundry/script/orderHook/02-DeployConstantProductPool.s.sol b/packages/foundry/script/orderHook/02-DeployConstantProductPool.s.sol new file mode 100755 index 00000000..0aa184e5 --- /dev/null +++ b/packages/foundry/script/orderHook/02-DeployConstantProductPool.s.sol @@ -0,0 +1,178 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TokenConfig, TokenType, LiquidityManagement, PoolRoleAccounts} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {IRateProvider} from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import {InputHelpers} from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; + +import {PoolHelpers, CustomPoolConfig, InitializationConfig} from "./PoolHelpers.sol"; + +import {ConstantProductFactory} from "../src/factories/ConstantProductFactory.sol"; +import {OrderHook} from "../src/OrderHook.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; + +import {Script, console} from "forge-std/Script.sol"; + +/** + * @title Deploy Constant Product Pool + * @notice Deploys, registers, and initializes a constant product pool that uses a Lottery Hook + */ +contract DeployConstantProductPool is PoolHelpers, Script { + function deployConstantProductPool( + address token1, + address token2 + ) public returns (address orderHook, address pool) { + HelperConfig helperConfig = new HelperConfig(); + + // Set the deployment configurations + CustomPoolConfig memory poolConfig = getProductPoolConfig( + token1, + token2 + ); + InitializationConfig memory initConfig = getProductPoolInitConfig( + token1, + token2 + ); + + // Start creating the transactions + address deployer = helperConfig.getConfig().account; + vm.startBroadcast(deployer); + + // Deploy a factory + ConstantProductFactory factory = new ConstantProductFactory( + IVault(helperConfig.getConfig().vault), + 365 days + ); //pauseWindowDuration + console.log( + "Constant Product Factory deployed at: %s", + address(factory) + ); + + // Deploy a hook + orderHook = address( + new OrderHook( + IVault(helperConfig.getConfig().vault), + address(factory), + helperConfig.getConfig().router, + IPermit2(helperConfig.getConfig().permit2) + ) + ); + console.log("OrderHook deployed at address: %s", orderHook); + + // Deploy a pool and register it with the vault + pool = factory.create( + poolConfig.name, + poolConfig.symbol, + poolConfig.salt, + poolConfig.tokenConfigs, + poolConfig.swapFeePercentage, + poolConfig.protocolFeeExempt, + poolConfig.roleAccounts, + orderHook, // poolHooksContract + poolConfig.liquidityManagement + ); + console.log("Constant Product Pool deployed at: %s", pool); + + // Approve the router to spend tokens for pool initialization + approveRouterWithPermit2(initConfig.tokens); + + // Seed the pool with initial liquidity using Router as entrypoint + router.initialize( + pool, + initConfig.tokens, + initConfig.exactAmountsIn, + initConfig.minBptAmountOut, + initConfig.wethIsEth, + initConfig.userData + ); + console.log("Constant Product Pool initialized successfully!"); + vm.stopBroadcast(); + } + + /** + * @dev Set all of the configurations for deploying and registering a pool here + * @notice TokenConfig encapsulates the data required for the Vault to support a token of the given type. + * For STANDARD tokens, the rate provider address must be 0, and paysYieldFees must be false. + * All WITH_RATE tokens need a rate provider, and may or may not be yield-bearing. + */ + function getProductPoolConfig( + address token1, + address token2 + ) internal view returns (CustomPoolConfig memory config) { + string memory name = "Constant Product Pool"; // name for the pool + string memory symbol = "CPP"; // symbol for the BPT + bytes32 salt = keccak256(abi.encode(block.number)); // salt for the pool deployment via factory + uint256 swapFeePercentage = 0.02e18; // 2% + bool protocolFeeExempt = false; + address poolHooksContract = address(0); // zero address if no hooks contract is needed + + TokenConfig[] memory tokenConfigs = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage + tokenConfigs[0] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token1), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + tokenConfigs[1] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token2), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + + PoolRoleAccounts memory roleAccounts = PoolRoleAccounts({ + pauseManager: address(0), // Account empowered to pause/unpause the pool (or 0 to delegate to governance) + swapFeeManager: address(0), // Account empowered to set static swap fees for a pool (or 0 to delegate to goverance) + poolCreator: address(0) // Account empowered to set the pool creator fee percentage + }); + LiquidityManagement memory liquidityManagement = LiquidityManagement({ + disableUnbalancedLiquidity: true, // Must be true to register pool with the Lottery Hook + enableAddLiquidityCustom: false, + enableRemoveLiquidityCustom: false, + enableDonation: false + }); + + config = CustomPoolConfig({ + name: name, + symbol: symbol, + salt: salt, + tokenConfigs: sortTokenConfig(tokenConfigs), + swapFeePercentage: swapFeePercentage, + protocolFeeExempt: protocolFeeExempt, + roleAccounts: roleAccounts, + poolHooksContract: poolHooksContract, + liquidityManagement: liquidityManagement + }); + } + + /** + * @dev Set the pool initialization configurations here + * @notice This is where the amounts of tokens to Seed the pool with initial liquidity using Router as entrypoint are set + */ + function getProductPoolInitConfig( + address token1, + address token2 + ) internal pure returns (InitializationConfig memory config) { + IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool + tokens[0] = IERC20(token1); + tokens[1] = IERC20(token2); + uint256[] memory exactAmountsIn = new uint256[](2); // Exact amounts of tokens to be added, sorted in token alphanumeric order + exactAmountsIn[0] = 50e18; // amount of token1 to send during pool initialization + exactAmountsIn[1] = 50e18; // amount of token2 to send during pool initialization + uint256 minBptAmountOut = 49e18; // Minimum amount of pool tokens to be received + bool wethIsEth = false; // If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens + bytes memory userData = bytes(""); // Additional (optional) data required for adding initial liquidity + + config = InitializationConfig({ + tokens: InputHelpers.sortTokens(tokens), + exactAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + wethIsEth: wethIsEth, + userData: userData + }); + } +} diff --git a/packages/foundry/script/orderHook/03-DeployWeightedPool8020 .s.sol b/packages/foundry/script/orderHook/03-DeployWeightedPool8020 .s.sol new file mode 100755 index 00000000..b2f95795 --- /dev/null +++ b/packages/foundry/script/orderHook/03-DeployWeightedPool8020 .s.sol @@ -0,0 +1,162 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TokenConfig, TokenType, LiquidityManagement, PoolRoleAccounts} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {IRateProvider} from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IRateProvider.sol"; +import {InputHelpers} from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; + +import {PoolHelpers, InitializationConfig} from "./PoolHelpers.sol"; +import {WeightedPoolFactory} from "lib/balancer-v3-monorepo/pkg/pool-weighted/contracts/WeightedPoolFactory.sol"; +import {OrderHook} from "../src/OrderHook.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; + +import {Script, console} from "forge-std/Script.sol"; + +/** + * @title Deploy Weighted Pool 80/20 + * @notice Deploys, registers, and initializes a 80/20 weighted pool that uses an Exit Fee Hook + */ +contract DeployWeightedPool8020 is PoolHelpers, Script { + function deployWeightedPool8020(address token1, address token2) public { + HelperConfig helperConfig = new HelperConfig(); + + // Set the pool initialization config + InitializationConfig memory initConfig = getWeightedPoolInitConfig( + token1, + token2 + ); + + // Start creating the transactions + address deployer = helperConfig.getConfig().account; + vm.startBroadcast(deployer); + + // Deploy a factory + WeightedPoolFactory factory = new WeightedPoolFactory( + vault, + 365 days, + "Factory v1", + "Pool v1" + ); + console.log("Weighted Pool Factory deployed at: %s", address(factory)); + + // Deploy a hook + address orderHook = address( + new OrderHook( + IVault(helperConfig.getConfig().vault), + address(factory), + helperConfig.getConfig().router, + IPermit2(helperConfig.getConfig().permit2) + ) + ); + console.log("ExitFeeHook deployed at address: %s", orderHook); + + // Deploy a pool and register it with the vault + /// @notice passing args directly to avoid stack too deep error + address pool = factory.create( + "80/20 Weighted Pool", // string name + "80-20-WP", // string symbol + getTokenConfigs(token1, token2), // TokenConfig[] tokenConfigs + getNormailzedWeights(), // uint256[] normalizedWeights + getRoleAccounts(), // PoolRoleAccounts roleAccounts + 0.03e18, // uint256 swapFeePercentage (3%) + orderHook, // address poolHooksContract + true, //bool enableDonation + true, // bool disableUnbalancedLiquidity (must be true for the ExitFee Hook) + keccak256(abi.encode(block.number)) // bytes32 salt + ); + console.log("Weighted Pool deployed at: %s", pool); + + // Approve the router to spend tokens for pool initialization + approveRouterWithPermit2(initConfig.tokens); + + // Seed the pool with initial liquidity using Router as entrypoint + router.initialize( + pool, + initConfig.tokens, + initConfig.exactAmountsIn, + initConfig.minBptAmountOut, + initConfig.wethIsEth, + initConfig.userData + ); + console.log("Weighted Pool initialized successfully!"); + vm.stopBroadcast(); + } + + /** + * @dev Set the token configs for the pool + * @notice TokenConfig encapsulates the data required for the Vault to support a token of the given type. + * For STANDARD tokens, the rate provider address must be 0, and paysYieldFees must be false. + * All WITH_RATE tokens need a rate provider, and may or may not be yield-bearing. + */ + function getTokenConfigs( + address token1, + address token2 + ) internal pure returns (TokenConfig[] memory tokenConfigs) { + tokenConfigs = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage + tokenConfigs[0] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token1), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + tokenConfigs[1] = TokenConfig({ // Make sure to have proper token order (alphanumeric) + token: IERC20(token2), + tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE + rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above) + paysYieldFees: false // Flag indicating whether yield fees should be charged on this token + }); + sortTokenConfig(tokenConfigs); + } + + /// @dev Set the weights for each token in the pool + function getNormailzedWeights() + internal + pure + returns (uint256[] memory normalizedWeights) + { + normalizedWeights = new uint256[](2); + normalizedWeights[0] = uint256(80e16); + normalizedWeights[1] = uint256(20e16); + } + + /// @dev Set the role accounts for the pool + function getRoleAccounts() + internal + pure + returns (PoolRoleAccounts memory roleAccounts) + { + roleAccounts = PoolRoleAccounts({ + pauseManager: address(0), // Account empowered to pause/unpause the pool (or 0 to delegate to governance) + swapFeeManager: address(0), // Account empowered to set static swap fees for a pool (or 0 to delegate to goverance) + poolCreator: address(0) // Account empowered to set the pool creator fee percentage + }); + } + + /// @dev Set the initialization config for the pool (i.e. the amount of tokens to be added) + function getWeightedPoolInitConfig( + address token1, + address token2 + ) internal pure returns (InitializationConfig memory config) { + IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool + tokens[0] = IERC20(token1); + tokens[1] = IERC20(token2); + uint256[] memory exactAmountsIn = new uint256[](2); // Exact amounts of tokens to be added, sorted in token alphanumeric order + exactAmountsIn[0] = 80e18; // amount of token1 to send during pool initialization + exactAmountsIn[1] = 20e18; // amount of token2 to send during pool initialization + uint256 minBptAmountOut = 49e18; // Minimum amount of pool tokens to be received + bool wethIsEth = false; // If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens + bytes memory userData = bytes(""); // Additional (optional) data required for adding initial liquidity + + config = InitializationConfig({ + tokens: InputHelpers.sortTokens(tokens), + exactAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + wethIsEth: wethIsEth, + userData: userData + }); + } +} diff --git a/packages/foundry/script/orderHook/Deploy.s.sol b/packages/foundry/script/orderHook/Deploy.s.sol new file mode 100644 index 00000000..45e2d8c3 --- /dev/null +++ b/packages/foundry/script/orderHook/Deploy.s.sol @@ -0,0 +1,40 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {DeployMockTokens} from "./00-DeployMockTokens.s.sol"; +import {DeployConstantSumPool} from "./01-DeployConstantSumPool.s.sol"; +import {DeployConstantProductPool} from "./02-DeployConstantProductPool.s.sol"; +import {DeployWeightedPool8020} from "./03-DeployWeightedPool8020 .s.sol"; + +import {Script} from "forge-std/Script.sol"; + +/** + * @title Deploy Script + * @dev Run all deploy scripts here to allow for scaffold integrations with nextjs front end + * @dev Run this script with `yarn deploy` + */ +contract DeployScript is Script { + function run() external { + DeployMockTokens deployTokens = new DeployMockTokens(); + DeployConstantSumPool deployConstantSumPool = new DeployConstantSumPool(); + DeployConstantProductPool deployConstantProductPool = new DeployConstantProductPool(); + //DeployWeightedPool8020 deployWeightedPool8020 = new DeployWeightedPool8020(); + + // Deploy mock tokens to use for the pools and hooks + (address mockToken1, address mockToken2, ) = deployTokens + .deployMockTokens(); + + // Deploy, register, and initialize a constant sum pool with a swap fee discount hook + deployConstantSumPool.deployConstantSumPool(mockToken1, mockToken2); + + // Deploy, register, and initialize a constant product pool with a lottery hook + deployConstantProductPool.deployConstantProductPool( + mockToken1, + mockToken2 + ); + + // Deploy, register, and initialize a weighted pool with an exit fee hook + //deployWeightedPool8020.deployWeightedPool8020(mockToken1, mockToken2); + } +} diff --git a/packages/foundry/script/orderHook/ExecuteOrder.s.sol b/packages/foundry/script/orderHook/ExecuteOrder.s.sol new file mode 100644 index 00000000..e6d3048f --- /dev/null +++ b/packages/foundry/script/orderHook/ExecuteOrder.s.sol @@ -0,0 +1,136 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {DeployMockTokens} from "./00-DeployMockTokens.s.sol"; +import {DeployConstantSumPool} from "./01-DeployConstantSumPool.s.sol"; +import {DeployConstantProductPool} from "./02-DeployConstantProductPool.s.sol"; + +import {OrderHook} from "../../contracts/hooks/OrderHook.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; + +import {Script, console} from "forge-std/Script.sol"; + +contract ExecuteOrder is Script { + function run() public { + executeOrder(); + } + + function executeOrder() public { + HelperConfig helperConfig = new HelperConfig(); + DeployMockTokens deployTokens = new DeployMockTokens(); + //DeployConstantSumPool deployConstantSumPool = new DeployConstantSumPool(); + DeployConstantProductPool deployConstantProductPool = new DeployConstantProductPool(); + + (address mockToken1, address mockToken2, ) = deployTokens + .deployMockTokens(); + + /* (address orderHook, address pool) = deployConstantSumPool + .deployConstantSumPool(mockToken1, mockToken2); */ + + (address orderHook, address pool) = deployConstantProductPool + .deployConstantProductPool(mockToken1, mockToken2); + + vm.startBroadcast(helperConfig.getConfig().account); + + bytes32 orderId = placeOrder( + mockToken1, + mockToken2, + helperConfig, + orderHook, + pool + ); + + console.log("Order ID:"); + console.logBytes32(orderId); + + console.log( + "Account MockToken1 balance after order: ", + IERC20(mockToken1).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + console.log( + "Account MockToken2 balance after order: ", + IERC20(mockToken2).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + + uint256[] memory poolPrice = IVault(helperConfig.getConfig().vault) + .getCurrentLiveBalances(pool); + + uint256 limit = ((((1e18 * poolPrice[0]) / poolPrice[1]) * (100 - 4)) / + 100); + + uint256 amountOut = IRouter(payable(helperConfig.getConfig().router)) + .swapSingleTokenExactIn( + pool, + IERC20(mockToken1), + IERC20(mockToken2), + 1e18, + limit, + block.timestamp + 1 hours, + false, + "" + ); + console.log("Amount out: ", amountOut / 1e16); + + console.log( + "Account MockToken1 balance after swap: ", + IERC20(mockToken1).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + console.log( + "Account MockToken2 balance after swap: ", + IERC20(mockToken2).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + + OrderHook(payable(orderHook)).executeOrder(orderId, ""); + + vm.stopBroadcast(); + + console.log( + "Account MockToken1 balance after order execution: ", + IERC20(mockToken1).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + console.log( + "Account MockToken2 balance after order execution: ", + IERC20(mockToken2).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + } + + function placeOrder( + address mockToken1, + address mockToken2, + HelperConfig helperConfig, + address orderHook, + address pool + ) public returns (bytes32 orderId) { + console.log( + "Account MockToken1 balance: ", + IERC20(mockToken1).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + console.log( + "Account MockToken2 balance: ", + IERC20(mockToken2).balanceOf(helperConfig.getConfig().account) / + 1e16 + ); + + IERC20(mockToken1).approve(orderHook, 1e18); + orderId = OrderHook(payable(orderHook)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + 1e18, + 1e18, + pool, + mockToken1, + mockToken2, + 4 + ); + } +} diff --git a/packages/foundry/script/orderHook/HelperConfig.s.sol b/packages/foundry/script/orderHook/HelperConfig.s.sol new file mode 100644 index 00000000..a0babb2e --- /dev/null +++ b/packages/foundry/script/orderHook/HelperConfig.s.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console} from "forge-std/Script.sol"; + +abstract contract CodeConstants { + address public FOUNDRY_DEFAULT_SENDER = + 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38; + + uint256 public constant ETH_SEPOLIA_CHAIN_ID = 11155111; + uint256 public constant ETH_MAINNET_CHAIN_ID = 1; + uint256 public constant LOCAL_CHAIN_ID = 31337; +} + +contract HelperConfig is Script, CodeConstants { + error HelperConfig__InvalidChainId(); + + struct NetworkConfig { + address account; + address vault; + address stablePoolFactory; + address weightedPoolFactory; + address router; + address permit2; + address DAI; + address WETH; + } + + NetworkConfig public localNetworkConfig; + mapping(uint256 chainId => NetworkConfig) public networkConfigs; + + constructor() { + networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getSepoliaEthConfig(); + } + + function getConfig() public returns (NetworkConfig memory) { + return getConfigByChainId(block.chainid); + } + + function setConfig( + uint256 chainId, + NetworkConfig memory networkConfig + ) public { + networkConfigs[chainId] = networkConfig; + } + + function getConfigByChainId( + uint256 chainId + ) public returns (NetworkConfig memory) { + if (chainId == ETH_SEPOLIA_CHAIN_ID) { + return networkConfigs[chainId]; + } else if (chainId == LOCAL_CHAIN_ID) { + return getOrCreateAnvilEthConfig(); + } else { + revert HelperConfig__InvalidChainId(); + } + } + + function getSepoliaEthConfig() + public + pure + returns (NetworkConfig memory sepoliaNetworkConfig) + { + sepoliaNetworkConfig = NetworkConfig({ + account: 0x05b8B8ed022FB7FC2e75983EAb003a6C7202E1A0, + vault: 0x7966FE92C59295EcE7FB5D9EfDB271967BFe2fbA, + stablePoolFactory: 0x4b4b45Edf6Ca26ae894377Cf4FeD1FA9F82D85C6, + weightedPoolFactory: 0x765ce16dbb3D7e89a9beBc834C5D6894e7fAA93c, + router: 0xDd10aDF05379D7C0Ee4bC9c72ecc5C01c40E25b8, + permit2: 0x000000000022D473030F116dDEE9F6B43aC78BA3, + DAI: 0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6, + WETH: 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9 + }); + } + + function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) { + localNetworkConfig = NetworkConfig({ + account: FOUNDRY_DEFAULT_SENDER, + vault: address(0), + stablePoolFactory: address(0), + weightedPoolFactory: address(0), + router: address(0), + permit2: address(0), + DAI: makeAddr("DAI"), + WETH: makeAddr("WETH") + }); + + return localNetworkConfig; + } +} diff --git a/packages/foundry/script/orderHook/PlaceOrder.s.sol b/packages/foundry/script/orderHook/PlaceOrder.s.sol new file mode 100644 index 00000000..15a912ab --- /dev/null +++ b/packages/foundry/script/orderHook/PlaceOrder.s.sol @@ -0,0 +1,57 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {DeployMockTokens} from "./00-DeployMockTokens.s.sol"; +import {DeployConstantSumPool} from "./01-DeployConstantSumPool.s.sol"; +import {DeployConstantProductPool} from "./02-DeployConstantProductPool.s.sol"; + +import {OrderHook} from "../../contracts/hooks/OrderHook.sol"; +import {HelperConfig} from "./HelperConfig.s.sol"; + +import {Script, console} from "forge-std/Script.sol"; + +contract PlaceOrder is Script { + function run() public { + placeOrder(); + } + + function placeOrder() public { + HelperConfig helperConfig = new HelperConfig(); + DeployMockTokens deployTokens = new DeployMockTokens(); + DeployConstantSumPool deployConstantSumPool = new DeployConstantSumPool(); + //DeployConstantProductPool deployConstantProductPool = new DeployConstantProductPool(); + + (address mockToken1, address mockToken2, ) = deployTokens + .deployMockTokens(); + + (address orderHook, address pool) = deployConstantSumPool + .deployConstantSumPool(mockToken1, mockToken2); + + console.log( + "Account balance: ", + IERC20(mockToken1).balanceOf(helperConfig.getConfig().account) / + 1e18 + ); + + vm.startBroadcast(helperConfig.getConfig().account); + IERC20(mockToken1).approve(orderHook, 1e18); + bytes32 orderId = OrderHook(payable(orderHook)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + 1e18, + 1e18, + pool, + mockToken1, + mockToken2, + 3 + ); + + console.log("Order ID:"); + console.logBytes32(orderId); + + /* deployConstantProductPool.deployConstantProductPool( + mockToken1, + mockToken2 + ); */ + } +} diff --git a/packages/foundry/script/orderHook/PoolHelpers.sol b/packages/foundry/script/orderHook/PoolHelpers.sol new file mode 100755 index 00000000..741f2162 --- /dev/null +++ b/packages/foundry/script/orderHook/PoolHelpers.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import {TokenConfig, LiquidityManagement, PoolRoleAccounts} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; + +/** + * @title Pool Helpers + * @notice Helpful types, interface instances, and functions for deploying pools on Balancer v3 + */ +contract PoolHelpers { + // BalancerV3 Sepolia addresses (7th testnet release) + IVault internal vault = IVault(0x7966FE92C59295EcE7FB5D9EfDB271967BFe2fbA); + IRouter internal router = + IRouter(0xDd10aDF05379D7C0Ee4bC9c72ecc5C01c40E25b8); + IPermit2 internal permit2 = + IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); + + /** + * Sorts the tokenConfig array into alphanumeric order + */ + function sortTokenConfig( + TokenConfig[] memory tokenConfig + ) internal pure returns (TokenConfig[] memory) { + for (uint256 i = 0; i < tokenConfig.length - 1; i++) { + for (uint256 j = 0; j < tokenConfig.length - i - 1; j++) { + if (tokenConfig[j].token > tokenConfig[j + 1].token) { + // Swap if they're out of order. + (tokenConfig[j], tokenConfig[j + 1]) = ( + tokenConfig[j + 1], + tokenConfig[j] + ); + } + } + } + return tokenConfig; + } + + /** + * @notice Approve permit2 on the token contract, then approve the router on the Permit2 contract + * @param tokens Array of tokens to approve the router to spend using Permit2 + */ + function approveRouterWithPermit2(IERC20[] memory tokens) internal { + approveSpenderOnToken(address(permit2), tokens); + approveSpenderOnPermit2(address(router), tokens); + } + + /** + * @notice Max approving to speed up UX on frontend + * @param spender Address of the spender + * @param tokens Array of tokens to approve + */ + function approveSpenderOnToken( + address spender, + IERC20[] memory tokens + ) internal { + uint256 maxAmount = type(uint256).max; + for (uint256 i = 0; i < tokens.length; ++i) { + tokens[i].approve(spender, maxAmount); + } + } + + /** + * @notice Max approving to speed up UX on frontend + * @param spender Address of the spender + * @param tokens Array of tokens to approve + */ + function approveSpenderOnPermit2( + address spender, + IERC20[] memory tokens + ) internal { + uint160 maxAmount = type(uint160).max; + uint48 maxExpiration = type(uint48).max; + for (uint256 i = 0; i < tokens.length; ++i) { + permit2.approve( + address(tokens[i]), + spender, + maxAmount, + maxExpiration + ); + } + } +} + +struct CustomPoolConfig { + string name; + string symbol; + bytes32 salt; + TokenConfig[] tokenConfigs; + uint256 swapFeePercentage; + bool protocolFeeExempt; + PoolRoleAccounts roleAccounts; + address poolHooksContract; + LiquidityManagement liquidityManagement; +} + +struct WeightedPoolConfig { + string name; + string symbol; + TokenConfig[] tokenConfigs; + uint256[] normalizedWeights; + PoolRoleAccounts roleAccounts; + uint256 swapFeePercentage; + address poolHooksContract; + bool enableDonation; + bool disableUnbalancedLiquidity; + bytes32 salt; +} + +struct InitializationConfig { + IERC20[] tokens; + uint256[] exactAmountsIn; + uint256 minBptAmountOut; + bool wethIsEth; + bytes userData; +} diff --git a/packages/foundry/test/OrderHookTest.t.sol b/packages/foundry/test/OrderHookTest.t.sol new file mode 100644 index 00000000..6caf346d --- /dev/null +++ b/packages/foundry/test/OrderHookTest.t.sol @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {IVault} from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import {HooksConfig, LiquidityManagement, PoolConfig, PoolRoleAccounts, TokenConfig} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; +import {IVaultErrors} from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; +import {IVaultAdmin} from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol"; +import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol"; +import {IVaultErrors} from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +import {CastingHelpers} from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol"; +import {ArrayHelpers} from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol"; +import {FixedPoint} from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; + +import {BaseVaultTest} from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol"; +import {PoolMock} from "@balancer-labs/v3-vault/contracts/test/PoolMock.sol"; +import {PoolFactoryMock} from "@balancer-labs/v3-vault/contracts/test/PoolFactoryMock.sol"; +import {RouterMock} from "@balancer-labs/v3-vault/contracts/test/RouterMock.sol"; + +import {OrderHook} from ".././contracts/hooks/OrderHook.sol"; + +contract OrderHookTest is BaseVaultTest { + using CastingHelpers for address[]; + using FixedPoint for uint256; + using ArrayHelpers for *; + + uint256 internal daiIdx; + uint256 internal usdcIdx; + + address payable internal trustedRouter; + + uint8 slippageTolerance = 3; + uint256 orderAmountIn = 1e20; + uint256 mockTriggerPrice = 1e20 + 1e19; + address badToken = makeAddr("badToken"); + address dummyUser = makeAddr("dummyUser"); + uint256 swapAmount = 1e19; + + function setUp() public override { + super.setUp(); + + (daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc)); + } + + function createHook() internal override returns (address) { + trustedRouter = payable(router); + // lp will be the owner of the hook. Only LP is able to set hook fee percentages. + vm.prank(lp); + address orderHook = address( + new OrderHook( + IVault(address(vault)), + address(factoryMock), + trustedRouter, + permit2 + ) + ); + vm.label(orderHook, "Order Hook"); + return orderHook; + } + + function testRegistryWithWrongFactory() public { + address orderPool = _createPoolToRegister(); + + TokenConfig[] memory tokenConfig = vault.buildTokenConfig( + [address(dai), address(usdc)].toMemoryArray().asIERC20() + ); + + uint32 pauseWindowEndTime = IVaultAdmin(address(vault)) + .getPauseWindowEndTime(); + uint32 bufferPeriodDuration = IVaultAdmin(address(vault)) + .getBufferPeriodDuration(); + uint32 pauseWindowDuration = pauseWindowEndTime - bufferPeriodDuration; + address unauthorizedFactory = address( + new PoolFactoryMock(IVault(address(vault)), pauseWindowDuration) + ); + + vm.expectRevert( + abi.encodeWithSelector( + IVaultErrors.HookRegistrationFailed.selector, + poolHooksContract, + orderPool, + unauthorizedFactory + ) + ); + + _registerPoolWithHook(orderPool, tokenConfig, unauthorizedFactory); + } + + function testCreationWithWrongFactory() public { + address orderPool = _createPoolToRegister(); + TokenConfig[] memory tokenConfig = vault.buildTokenConfig( + [address(dai), address(usdc)].toMemoryArray().asIERC20() + ); + + vm.expectRevert( + abi.encodeWithSelector( + IVaultErrors.HookRegistrationFailed.selector, + poolHooksContract, + orderPool, + address(factoryMock) + ) + ); + _registerPoolWithHook(orderPool, tokenConfig, address(factoryMock)); + } + + function testSuccessfulRegistry() public { + // Registering with allowed factory + address orderPool = factoryMock.createPool("Test Pool", "TEST"); + TokenConfig[] memory tokenConfig = vault.buildTokenConfig( + [address(dai), address(usdc)].toMemoryArray().asIERC20() + ); + + _registerPoolWithHook(orderPool, tokenConfig, address(factoryMock)); + + HooksConfig memory hooksConfig = vault.getHooksConfig(orderPool); + + assertEq( + hooksConfig.hooksContract, + poolHooksContract, + "Wrong poolHooksContract" + ); + assertEq( + hooksConfig.shouldCallAfterSwap, + true, + "shouldCallComputeDynamicSwapFee is false" + ); + } + + function testGetVaultPrice() public view { + uint256[] memory balancesLiveScaled18 = OrderHook( + payable(poolHooksContract) + ).getPoolPrice(pool); + + ( + IERC20[] memory poolTokens, + , + , + uint256[] memory lastBalancesLiveScaled18 + ) = vault.getPoolTokenInfo(pool); + + for (uint256 i = 0; i < poolTokens.length; i++) { + assertEq(lastBalancesLiveScaled18[i], balancesLiveScaled18[i]); + } + } + + /*///////////////////////////////// + ///// PLACE ORDER TESTS ///// + ////////////////////////////////*/ + + function testPlaceOrderRevertsForZeroFund() public { + vm.expectRevert( + abi.encodeWithSelector( + OrderHook.OrderHook__OrderAmountInMustBeMoreThanZero.selector + ) + ); + OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + 0, + mockTriggerPrice, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + } + + function testRevertsIfTokenNotInPool() public { + vm.expectRevert( + abi.encodeWithSelector( + IVaultErrors.TokenNotRegistered.selector, + IERC20(badToken) + ) + ); + OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + mockTriggerPrice, + pool, + address(tokens[0]), + badToken, + slippageTolerance + ); + } + + function testRevertsIfNotEnoughAllowance() public { + uint256 currentAllowance = tokens[0].allowance( + users[2], + poolHooksContract + ); + vm.prank(users[2]); + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientAllowance.selector, + poolHooksContract, + currentAllowance, + orderAmountIn + ) + ); + OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + mockTriggerPrice, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + } + + function testRevertsIfNotEnoughBalance() public { + uint256 fromBalance = tokens[0].balanceOf(dummyUser); + vm.prank(dummyUser); + tokens[0].approve(poolHooksContract, orderAmountIn); + + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + dummyUser, + fromBalance, + orderAmountIn + ) + ); + vm.prank(dummyUser); + OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + mockTriggerPrice, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + } + + function testOrderPlacedSuccessfully() public { + uint256 initialUserBalance = tokens[0].balanceOf(users[2]); + uint256 orderCountBefore = OrderHook(payable(poolHooksContract)) + .orderCount(); + + vm.prank(users[2]); + tokens[0].approve(poolHooksContract, orderAmountIn); + + vm.prank(users[2]); + bytes32 orderId = OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + mockTriggerPrice, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + + assertEq( + tokens[0].balanceOf(poolHooksContract), + orderAmountIn, + "Incorrect balance of token0" + ); + + assertEq( + tokens[0].balanceOf(users[2]) + orderAmountIn, + initialUserBalance + ); + + OrderHook.Order memory order = OrderHook(payable(poolHooksContract)) + .getOrderById(orderId); + + vm.prank(users[2]); + OrderHook.Order[] memory userOrders = OrderHook( + payable(poolHooksContract) + ).getUserOrders(); + + assertEq(abi.encode(order.orderId), abi.encode(userOrders[0].orderId)); + assertEq(order.orderId, userOrders[0].orderId); + + assertEq( + orderCountBefore + 1, + OrderHook(payable(poolHooksContract)).orderCount() + ); + } + + /*///////////////////////////////// + ///// CANCEL ORDER TESTS ///// + ////////////////////////////////*/ + + function testRevertsIfNotOwner() public { + vm.prank(users[2]); + tokens[0].approve(poolHooksContract, orderAmountIn); + + vm.prank(users[2]); + bytes32 orderId = OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + mockTriggerPrice, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + + vm.expectRevert( + abi.encodeWithSelector( + OrderHook.OrderHook__UnauthorizedCaller.selector + ) + ); + OrderHook(payable(poolHooksContract)).cancelOrder(orderId); + } + + function testRevertsIfOrderNotExists() public { + vm.expectRevert( + abi.encodeWithSelector( + OrderHook.OrderHook__UnauthorizedCaller.selector + ) + ); + bytes32 fakeOrderId = keccak256( + abi.encodePacked(users[2], block.timestamp) + ); + OrderHook(payable(poolHooksContract)).cancelOrder(fakeOrderId); + } + + function testCancelOrderSuccessfully() public { + uint256 initialBalance = tokens[0].balanceOf(users[2]); + + vm.startPrank(users[2]); + tokens[0].approve(poolHooksContract, orderAmountIn); + + bytes32 orderId = OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + mockTriggerPrice, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + + assertEq(initialBalance - orderAmountIn, tokens[0].balanceOf(users[2])); + + OrderHook(payable(poolHooksContract)).cancelOrder(orderId); + vm.stopPrank(); + + assertEq(initialBalance, tokens[0].balanceOf(users[2])); + } + + /*///////////////////////////////// + ///// AFTER SWAP HOOk TESTS ///// + ////////////////////////////////*/ + + function testRevertsIfRouterNotTrusted() public { + address payable untrustedRouter = payable( + new RouterMock(IVault(address(vault)), weth, permit2) + ); + + vm.prank(users[2]); + vm.expectRevert( + abi.encodeWithSelector( + OrderHook.OrderHook__UnauthorizedRouter.selector + ) + ); + RouterMock(untrustedRouter).swapSingleTokenExactIn( + pool, + dai, + usdc, + swapAmount, + swapAmount, + block.timestamp + 1 hours, + false, + bytes("") + ); + } + + function testHookEmitsEventsAfterSwap() public { + _doOrder(); + + vm.recordLogs(); + vm.prank(users[2]); + RouterMock(trustedRouter).swapSingleTokenExactIn( + pool, + dai, + usdc, + swapAmount, + swapAmount, + block.timestamp + 1 hours, + false, + bytes("") + ); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 eventSig = keccak256( + "AfterSwapPrice(address,address,uint256,uint256,uint256,uint256)" + ); + + // Iterate over the logs to find and decode the AfterSwapPrice event + for (uint256 i = 0; i < logs.length; i++) { + Vm.Log memory log = logs[i]; + + // Check if the event matches the signature + if (log.topics[0] == eventSig) { + // Decode event parameters + address tokenIn = address(uint160(uint256(log.topics[1]))); + address tokenOut = address(uint160(uint256(log.topics[2]))); + + // Assert that the emitted values match your expectations + assertEq(tokenIn, address(dai)); + assertEq(tokenOut, address(usdc), "Unexpected tokenOut"); + } + } + } + + /*///////////////////////////////// + ///// EXECUTE ORDER TESTS ///// + ////////////////////////////////*/ + + function testRevertsIfOrderNotExistsForExecution() public { + vm.expectRevert( + abi.encodeWithSelector( + OrderHook.OrderHook__UnauthorizedCaller.selector + ) + ); + bytes32 fakeOrderId = keccak256( + abi.encodePacked(users[2], block.timestamp) + ); + OrderHook(payable(poolHooksContract)).executeOrder(fakeOrderId, ""); + } + + function testEmitsEventOnceExecuted() public { + bytes32[] memory orderIds = _doOrder(); + + uint256 initialBalance = tokens[1].balanceOf(users[2]); + console.log("Initial user balance: ", initialBalance); + console.log( + "Initial hook balance: ", + tokens[0].balanceOf(poolHooksContract) + ); + + vm.expectEmit(); + emit OrderHook.OrderExecuted(orderIds[0], users[2]); + vm.prank(users[2]); + OrderHook(payable(poolHooksContract)).executeOrder(orderIds[0], ""); + + console.log( + "New hook balance: ", + tokens[0].balanceOf(poolHooksContract) + ); + console.log("New user balance: ", tokens[1].balanceOf(users[2])); + + assertEq(0, tokens[0].balanceOf(poolHooksContract)); + assert(initialBalance < tokens[1].balanceOf(users[2])); + } + + /*///////////////////////////////////// + /////// FUNCTIONALITY TESTS /////// + /////////////////////////////////////*/ + function testUserCanSwap() public { + uint256 initialBalance1 = tokens[0].balanceOf(users[2]); + uint256 initialBalance2 = tokens[1].balanceOf(users[2]); + vm.startPrank(users[2]); + + uint256 amountOut = RouterMock(trustedRouter).swapSingleTokenExactIn( + pool, + dai, + usdc, + swapAmount, + swapAmount, + block.timestamp + 1 hours, + false, + bytes("") + ); + + vm.stopPrank(); + assertEq(initialBalance1 - swapAmount, tokens[0].balanceOf(users[2])); + assertEq(initialBalance2 + amountOut, tokens[1].balanceOf(users[2])); + } + + function testPoolHookCanSwap() public { + vm.prank(users[2]); + tokens[0].transfer(poolHooksContract, swapAmount); + + assertEq(swapAmount, tokens[0].balanceOf(poolHooksContract)); + console.log( + "hook balance 1 before: ", + tokens[0].balanceOf(poolHooksContract) + ); + console.log( + "hook balance 2 before: ", + tokens[1].balanceOf(poolHooksContract) + ); + + vm.startPrank(poolHooksContract); + tokens[0].approve(address(permit2), swapAmount); + permit2.approve( + address(tokens[0]), + address(router), + uint160(swapAmount), + type(uint48).max + ); + RouterMock(trustedRouter).swapSingleTokenExactIn( + pool, + dai, + usdc, + swapAmount, + swapAmount, + block.timestamp + 1 hours, + false, + bytes("") + ); + + vm.stopPrank(); + + console.log( + "hook balance 1 after: ", + tokens[0].balanceOf(poolHooksContract) + ); + console.log( + "hook balance 2 after: ", + tokens[1].balanceOf(poolHooksContract) + ); + + assertEq(swapAmount, tokens[1].balanceOf(poolHooksContract)); + } + + /*///////////////////////////////////// + /////// INTERNAL FUNCTIONS /////// + /////////////////////////////////////*/ + + function _doOrder() private returns (bytes32[] memory) { + bytes32[] memory orderIds = new bytes32[](2); + // Order 1 TAKE_PROFIT + vm.startPrank(users[2]); + tokens[0].approve(poolHooksContract, orderAmountIn); + tokens[1].approve(poolHooksContract, orderAmountIn); + + uint256[] memory balancesLiveScaled18 = OrderHook( + payable(poolHooksContract) + ).getPoolPrice(pool); + + orderIds[0] = OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.TAKE_PROFIT, + orderAmountIn, + balancesLiveScaled18[0] + 1e19, + pool, + address(tokens[0]), + address(tokens[1]), + slippageTolerance + ); + + RouterMock(trustedRouter).swapSingleTokenExactIn( + pool, + dai, + usdc, + swapAmount, + swapAmount, + block.timestamp + 1 hours, + false, + bytes("") + ); + + // Order 2 STOP_LOSS + orderIds[1] = OrderHook(payable(poolHooksContract)).placeOrder( + OrderHook.OrderType.STOP_LOSS, + orderAmountIn, + balancesLiveScaled18[1] - 1e19, + pool, + address(tokens[1]), + address(tokens[0]), + slippageTolerance + ); + vm.stopPrank(); + + return orderIds; + } + + function _createPoolToRegister() private returns (address newPool) { + newPool = address( + new PoolMock(IVault(address(vault)), "Order pool", "orderPool") + ); + vm.label(newPool, "Order pool"); + } + + function _registerPoolWithHook( + address orderPool, + TokenConfig[] memory tokenConfig, + address factory + ) private { + PoolRoleAccounts memory roleAccounts; + LiquidityManagement memory liquidityManagement; + + PoolFactoryMock(factory).registerPool( + orderPool, + tokenConfig, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } +}