diff --git a/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol b/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol index 806d9c1e..3ce87035 100644 --- a/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol +++ b/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol @@ -9,8 +9,8 @@ import { TokenConfig, LiquidityManagement } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; -import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; import { IBasePool } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePool.sol"; +import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol"; import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -19,54 +19,70 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * @notice Applies a 50% discount to the swap fee for users holding veBAL tokens */ contract VeBALFeeDiscountHook is BaseHooks { - // only pools from the allowedFactory are able to register and use this hook address private immutable _allowedFactory; - // 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 _trustedRouter; IERC20 private immutable _veBAL; - constructor(IVault vault, address allowedFactory, address veBAL, address trustedRouter) BaseHooks(vault) { + constructor(IVault vault, address allowedFactory, address trustedRouter, IERC20 veBAL) BaseHooks(vault) { _allowedFactory = allowedFactory; _trustedRouter = trustedRouter; - _veBAL = IERC20(veBAL); - } - - /// @inheritdoc IHooks - function getHookFlags() external pure override returns (IHooks.HookFlags memory hookFlags) { - hookFlags.shouldCallComputeDynamicSwapFee = true; + _veBAL = veBAL; } - /// @inheritdoc IHooks + /** + * @notice Hook executed when pool is registered + * @dev Return true if registration was successful + * @dev Return false to revert the registration of the pool + * @dev Vault address can be accessed with msg.sender + * @param factory Address of the pool factory + * @param pool Address of the pool + * @return success True if the hook allowed the registration, false otherwise + */ function onRegister( address factory, address pool, TokenConfig[] memory, LiquidityManagement calldata ) external view override 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. Since we only use onComputeDynamicSwapFee, this might be an - // overkill in real applications because the pool math doesn't play a role in the discount calculation. + // Only pools deployed by an allowed factory may register return factory == _allowedFactory && IBasePoolFactory(factory).isPoolFromFactory(pool); } + /** + * @notice Returns flags informing which hooks are implemented in the contract. + * @return hookFlags Flags indicating which hooks the contract supports + */ + function getHookFlags() external pure override returns (IHooks.HookFlags memory hookFlags) { + // Support the `onComputeDynamicSwapFeePercentage` hook + hookFlags.shouldCallComputeDynamicSwapFee = true; + } + + /** + * @notice Called before `onBeforeSwap` if the pool has dynamic fees. + * @param params Swap parameters (see IBasePool.PoolSwapParams for struct definition) + * @param staticSwapFeePercentage Value of the static swap fee, for reference + * @return success True if the pool wishes to proceed with settlement + * @return dynamicSwapFee Value of the swap fee + */ function onComputeDynamicSwapFee( IBasePool.PoolSwapParams calldata params, - address, + address, // pool uint256 staticSwapFeePercentage - ) external view override returns (bool, uint256) { - // If the router is not trusted, does not apply the veBAL discount because getSender() may be manipulated by a malicious router. + ) external view override returns (bool success, uint256 dynamicSwapFee) { + // If the router is not trusted, do not apply a fee discount if (params.router != _trustedRouter) { return (true, staticSwapFeePercentage); } + // Find the user's address address user = IRouterCommon(params.router).getSender(); - // If user has veBAL, apply a 50% discount to the current fee (divides fees by 2) + // If the user owns veBAL, apply a 50% discount to the swap fee if (_veBAL.balanceOf(user) > 0) { return (true, staticSwapFeePercentage / 2); } + // Otherwise, do not apply the discount return (true, staticSwapFeePercentage); } } diff --git a/packages/foundry/script/Deploy.s.sol b/packages/foundry/script/Deploy.s.sol index 7dd86dea..1f7d1433 100644 --- a/packages/foundry/script/Deploy.s.sol +++ b/packages/foundry/script/Deploy.s.sol @@ -1,27 +1,30 @@ //SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; - +import { ScaffoldHelpers } from "./ScaffoldHelpers.sol"; import { DeployMockTokens } from "./DeployMockTokens.s.sol"; import { DeployConstantSum } from "./DeployConstantSum.s.sol"; import { DeployConstantProduct } from "./DeployConstantProduct.s.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; /** * @title Deploy Script * @notice Import all deploy scripts here so that scaffold can exportDeployments() * @dev Run this script with `yarn deploy` */ -contract DeployScript is DeployMockTokens, DeployConstantSum, DeployConstantProduct { +contract DeployScript is ScaffoldHelpers { function run() external virtual { // Deploy mock tokens to be used for pools and hooks contracts - (IERC20 mockToken1, IERC20 mockToken2, IERC20 mockVeBAL) = deployMockTokens(); + DeployMockTokens deployMockTokens = new DeployMockTokens(); + (IERC20 mockToken1, IERC20 mockToken2, IERC20 mockVeBAL) = deployMockTokens.run(); // Deploy a constant sum factory and a pool - deployConstantSum(mockToken1, mockToken2); + DeployConstantSum deployConstantSum = new DeployConstantSum(); + deployConstantSum.run(mockToken1, mockToken2); // Deploy a constant product factory, a hooks contract, and a pool - deployConstantProduct(mockToken1, mockToken2, mockVeBAL); + DeployConstantProduct deployConstantProduct = new DeployConstantProduct(); + deployConstantProduct.run(mockToken1, mockToken2, mockVeBAL); /** * This function generates the file containing the contracts Abi definitions that are carried from /foundry to /nextjs. diff --git a/packages/foundry/script/DeployConstantProduct.s.sol b/packages/foundry/script/DeployConstantProduct.s.sol index de5da367..c4908915 100644 --- a/packages/foundry/script/DeployConstantProduct.s.sol +++ b/packages/foundry/script/DeployConstantProduct.s.sol @@ -22,11 +22,11 @@ import { ConstantProductFactory } from "../contracts/pools/ConstantProductFactor * @notice Deploys a factory and hooks contract and then deploys, registers, and initializes a constant product pool */ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { - function deployConstantProduct(IERC20 token1, IERC20 token2, IERC20 veBAL) internal { + function run(IERC20 token1, IERC20 token2, IERC20 veBAL) external virtual { // Set the deployment configurations uint32 pauseWindowDuration = 365 days; - RegistrationConfig memory regConfig = getPoolRegistrationConfig(token1, token2); - InitializationConfig memory initConfig = getPoolInitializationConfig(token1, token2); + PoolRegistrationConfig memory regConfig = getPoolRegistrationConfig(token1, token2); + PoolInitializationConfig memory initConfig = getPoolInitializationConfig(token1, token2); // Start creating the transactions uint256 deployerPrivateKey = getDeployerPrivateKey(); @@ -40,8 +40,8 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { VeBALFeeDiscountHook poolHooksContract = new VeBALFeeDiscountHook( IVault(vault), address(factory), - address(veBAL), - address(router) + address(router), + IERC20(veBAL) ); console.log("VeBALFeeDiscountHook deployed at address: %s", address(poolHooksContract)); @@ -59,8 +59,14 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { ); console.log("Constant Product Pool deployed at: %s", pool); + // Approve Permit2 contract to spend tokens on behalf of deployer + approveSpenderOnToken(address(permit2), initConfig.tokens); + + // Approve Router contract to spend tokens using Permit2 + approveSpenderOnPermit2(address(router), initConfig.tokens); + // Seed the pool with initial liquidity - initializePool( + router.initialize( pool, initConfig.tokens, initConfig.exactAmountsIn, @@ -74,15 +80,14 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { /** * @dev Set all of the configurations for deploying and registering a pool here - * - * TokenConfig encapsulates the data required for the Vault to support a token of the given type. + * @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 getPoolRegistrationConfig( IERC20 token1, IERC20 token2 - ) internal view returns (RegistrationConfig memory regConfig) { + ) internal view returns (PoolRegistrationConfig 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 @@ -90,7 +95,7 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { bool protocolFeeExempt = false; address poolHooksContract = address(0); // zero address if no hooks contract is needed - TokenConfig[] memory tokenConfig = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage. + TokenConfig[] memory tokenConfig = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage tokenConfig[0] = TokenConfig({ // Make sure to have proper token order (alphanumeric) token: token1, tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE @@ -116,7 +121,7 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { enableDonation: false }); - regConfig = RegistrationConfig({ + config = PoolRegistrationConfig({ name: name, symbol: symbol, salt: salt, @@ -136,7 +141,7 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { function getPoolInitializationConfig( IERC20 token1, IERC20 token2 - ) internal pure returns (InitializationConfig memory poolInitConfig) { + ) internal pure returns (PoolInitializationConfig memory config) { IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool tokens[0] = token1; tokens[1] = token2; @@ -147,7 +152,7 @@ contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers { 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 - poolInitConfig = InitializationConfig({ + config = PoolInitializationConfig({ tokens: InputHelpers.sortTokens(tokens), exactAmountsIn: exactAmountsIn, minBptAmountOut: minBptAmountOut, diff --git a/packages/foundry/script/DeployConstantSum.s.sol b/packages/foundry/script/DeployConstantSum.s.sol index 686f2ed2..559d914f 100644 --- a/packages/foundry/script/DeployConstantSum.s.sol +++ b/packages/foundry/script/DeployConstantSum.s.sol @@ -10,7 +10,6 @@ import { import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/vault/IRateProvider.sol"; import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol"; import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; -import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; import { PoolHelpers } from "./PoolHelpers.sol"; import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol"; @@ -21,18 +20,18 @@ import { ConstantSumFactory } from "../contracts/pools/ConstantSumFactory.sol"; * @notice Deploys, registers, and initializes a Constant Sum Pool */ contract DeployConstantSum is PoolHelpers, ScaffoldHelpers { - function deployConstantSum(IERC20 token1, IERC20 token2) internal { + function run(IERC20 token1, IERC20 token2) external virtual { // Set the deployment configurations uint32 pauseWindowDuration = 365 days; - RegistrationConfig memory regConfig = getRegistrationConfig(token1, token2); - InitializationConfig memory initConfig = getInitializationConfig(token1, token2); + PoolRegistrationConfig memory regConfig = getPoolRegistrationConfig(token1, token2); + PoolInitializationConfig memory initConfig = getPoolInitializationConfig(token1, token2); // Start creating the transactions uint256 deployerPrivateKey = getDeployerPrivateKey(); vm.startBroadcast(deployerPrivateKey); // Deploy a constant sum factory contract - ConstantSumFactory factory = new ConstantSumFactory(IVault(vault), pauseWindowDuration); + ConstantSumFactory factory = new ConstantSumFactory(vault, pauseWindowDuration); console.log("Constant Sum Factory deployed at: %s", address(factory)); // Deploy a pool and register it with the vault @@ -49,8 +48,14 @@ contract DeployConstantSum is PoolHelpers, ScaffoldHelpers { ); console.log("Constant Sum Pool deployed at: %s", pool); + // Approve Permit2 contract to spend tokens on behalf of deployer + approveSpenderOnToken(address(permit2), initConfig.tokens); + + // Approve Router contract to spend tokens using Permit2 + approveSpenderOnPermit2(address(router), initConfig.tokens); + // Seed the pool with initial liquidity - initializePool( + router.initialize( pool, initConfig.tokens, initConfig.exactAmountsIn, @@ -68,10 +73,10 @@ contract DeployConstantSum is PoolHelpers, ScaffoldHelpers { * 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 getRegistrationConfig( + function getPoolRegistrationConfig( IERC20 token1, IERC20 token2 - ) internal view returns (RegistrationConfig memory regConfig) { + ) internal view returns (PoolRegistrationConfig 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 @@ -105,7 +110,7 @@ contract DeployConstantSum is PoolHelpers, ScaffoldHelpers { enableDonation: false }); - regConfig = RegistrationConfig({ + config = PoolRegistrationConfig({ name: name, symbol: symbol, salt: salt, @@ -122,10 +127,10 @@ contract DeployConstantSum is PoolHelpers, ScaffoldHelpers { * @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 getInitializationConfig( + function getPoolInitializationConfig( IERC20 token1, IERC20 token2 - ) internal pure returns (InitializationConfig memory poolInitConfig) { + ) internal pure returns (PoolInitializationConfig memory config) { IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool tokens[0] = token1; tokens[1] = token2; @@ -136,7 +141,7 @@ contract DeployConstantSum is PoolHelpers, ScaffoldHelpers { 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 - poolInitConfig = InitializationConfig({ + config = PoolInitializationConfig({ tokens: InputHelpers.sortTokens(tokens), exactAmountsIn: exactAmountsIn, minBptAmountOut: minBptAmountOut, diff --git a/packages/foundry/script/DeployMockTokens.s.sol b/packages/foundry/script/DeployMockTokens.s.sol index 4e58bb76..dfebe5c8 100644 --- a/packages/foundry/script/DeployMockTokens.s.sol +++ b/packages/foundry/script/DeployMockTokens.s.sol @@ -14,7 +14,7 @@ import { MockVeBAL } from "../contracts/mocks/MockVeBAL.sol"; * @notice Deploys mock tokens for use with pools and hooks */ contract DeployMockTokens is ScaffoldHelpers { - function deployMockTokens() internal returns (IERC20 mockToken1, IERC20 mockToken2, IERC20 mockVeBAL) { + function run() external virtual returns (IERC20 mockToken1, IERC20 mockToken2, IERC20 mockVeBAL) { uint256 deployerPrivateKey = getDeployerPrivateKey(); vm.startBroadcast(deployerPrivateKey); diff --git a/packages/foundry/script/PoolHelpers.sol b/packages/foundry/script/PoolHelpers.sol index 51cf9d87..1e8e24d4 100644 --- a/packages/foundry/script/PoolHelpers.sol +++ b/packages/foundry/script/PoolHelpers.sol @@ -9,17 +9,15 @@ import { 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"; /** - * A collection of addresses and helper functions for deploying pools + * @title Pool Helpers + * @notice Helpful types, interface instances, and functions for deploying pools on Balancer v3 */ contract PoolHelpers { - // BalancerV3 Sepolia addresses (5th testnet release) - address internal vault = 0x92B5c1CB2999c45804A60d6529D77DeEF00fb839; - address internal router = 0xa12Da7dfD0792a10a5b05B575545Bd685798Ce35; - address internal permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - - struct RegistrationConfig { + struct PoolRegistrationConfig { string name; string symbol; bytes32 salt; @@ -31,7 +29,7 @@ contract PoolHelpers { LiquidityManagement liquidityManagement; } - struct InitializationConfig { + struct PoolInitializationConfig { IERC20[] tokens; uint256[] exactAmountsIn; uint256 minBptAmountOut; @@ -39,24 +37,10 @@ contract PoolHelpers { bytes userData; } - /** - * @notice Approves the vault to spend tokens and then initializes the pool - */ - function initializePool( - address pool, - IERC20[] memory tokens, - uint256[] memory exactAmountsIn, - uint256 minBptAmountOut, - bool wethIsEth, - bytes memory userData - ) internal { - // Approve Permit2 to spend account tokens - approveSpenderOnToken(address(permit2), tokens); - // Approve Router to spend account tokens using Permit2 - approveSpenderOnPermit2(address(router), tokens); - // Initialize pool with the tokens that have been permitted - IRouter(router).initialize(pool, tokens, exactAmountsIn, minBptAmountOut, wethIsEth, userData); - } + // BalancerV3 Sepolia addresses (6th testnet release) + IVault internal vault = IVault(0x92B5c1CB2999c45804A60d6529D77DeEF00fb839); + IRouter internal router = IRouter(0xa12Da7dfD0792a10a5b05B575545Bd685798Ce35); + IPermit2 internal permit2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); /** * Sorts the tokenConfig array into alphanumeric order @@ -94,7 +78,7 @@ contract PoolHelpers { uint160 maxAmount = type(uint160).max; uint48 maxExpiration = type(uint48).max; for (uint256 i = 0; i < tokens.length; ++i) { - IPermit2(permit2).approve(address(tokens[i]), spender, maxAmount, maxExpiration); + permit2.approve(address(tokens[i]), spender, maxAmount, maxExpiration); } } } diff --git a/packages/foundry/test/VeBALFeeDiscountHook.t.sol b/packages/foundry/test/VeBALFeeDiscountHook.t.sol index bbdce7a7..24bc21c6 100644 --- a/packages/foundry/test/VeBALFeeDiscountHook.t.sol +++ b/packages/foundry/test/VeBALFeeDiscountHook.t.sol @@ -23,6 +23,7 @@ import { PoolFactoryMock } from "@balancer-labs/v3-vault/contracts/test/PoolFact import { RouterMock } from "@balancer-labs/v3-vault/contracts/test/RouterMock.sol"; import { VeBALFeeDiscountHook } from "../contracts/hooks/VeBALFeeDiscountHook.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract VeBALFeeDiscountHookTest is BaseVaultTest { using FixedPoint for uint256; @@ -48,7 +49,7 @@ contract VeBALFeeDiscountHookTest is BaseVaultTest { // lp will be the owner of the hook. Only LP is able to set hook fee percentages. vm.prank(lp); address veBalFeeHook = address( - new VeBALFeeDiscountHook(IVault(address(vault)), address(factoryMock), address(veBAL), trustedRouter) + new VeBALFeeDiscountHook(IVault(address(vault)), address(factoryMock), trustedRouter, IERC20(veBAL)) ); vm.label(veBalFeeHook, "VeBAL Fee Hook"); return veBalFeeHook; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index c513effa..8e0ef77f 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -1865,14 +1865,14 @@ const deployedContracts = { internalType: "address", }, { - name: "veBAL", + name: "trustedRouter", type: "address", internalType: "address", }, { - name: "trustedRouter", + name: "veBAL", type: "address", - internalType: "address", + internalType: "contract IERC20", }, ], stateMutability: "nonpayable", @@ -2417,12 +2417,12 @@ const deployedContracts = { ], outputs: [ { - name: "", + name: "success", type: "bool", internalType: "bool", }, { - name: "", + name: "dynamicSwapFee", type: "uint256", internalType: "uint256", }, @@ -2519,19 +2519,7 @@ const deployedContracts = { ], }, ], - inheritedFunctions: { - getHookFlags: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onAfterAddLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onAfterInitialize: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onAfterRemoveLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onAfterSwap: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onBeforeAddLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onBeforeInitialize: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onBeforeRemoveLiquidity: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onBeforeSwap: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onComputeDynamicSwapFee: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - onRegister: "lib/balancer-v3-monorepo/pkg/vault/contracts/BaseHooks.sol", - }, + inheritedFunctions: {}, }, }, } as const;