From cffd94c9e4b38a64207d394a57b0f45edb3dc671 Mon Sep 17 00:00:00 2001 From: Matthew Pereira Date: Sat, 20 Jul 2024 16:23:36 -0700 Subject: [PATCH] Update tests --- README.md | 14 +- .../contracts/hooks/VeBALFeeDiscountHook.sol | 5 +- .../foundry/test/ConstantSumFactory.t.sol | 3 +- packages/foundry/test/ConstantSumPool.t.sol | 3 - .../foundry/test/VeBALFeeDiscountHook.t.sol | 231 ++++++++++++++++++ 5 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 packages/foundry/test/VeBALFeeDiscountHook.t.sol diff --git a/README.md b/README.md index ccbd1bd1..84d02c85 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ The deploy scripts are all located in the [foundry/script/](https://github.com/b #### `00_DeploySetup.s.sol` -Deploys mock tokens, factory contracts, and hooks contracts to be used by pools +Deploy mock tokens, factory contracts, and hooks contracts to be used by pools - Set the `pauseWindowDuration` for the factory contracts - Set the mock token names, symbols, and supply @@ -214,14 +214,14 @@ Deploys mock tokens, factory contracts, and hooks contracts to be used by pools #### `01_DeployConstantSumPool.s.sol` -Deploys, registers, and initializes a Constant Sum Pool +Deploy, register, and initialize a Constant Sum Pool - Set the pool registration config in the `getRegistrationConfig()` function - Set the pool initialization config in the `getInitializationConfig()` function #### `02_DeployConstantProductPool.s.sol` -Deploys, registers, and initializes a Constant Product Pool +Deploy, register, and initialize a Constant Product Pool - Set the pool registration config in the `getRegistrationConfig()` function - Set the pool initialization config in the `getInitializationConfig()` function @@ -257,7 +257,7 @@ yarn deploy:product ## 5. Test the Contracts 🧪 -Sample tests for the `ConstantSumPool` and `ConstantSumFactory` are provided as examples to help you get started writing your own tests. +The [balancer-v3-monorepo](https://github.com/balancer/balancer-v3-monorepo) provides testing utility contracts like [BaseVaultTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/vault/test/foundry/utils/BaseVaultTest.sol). Therefore, the best way to begin writing tests for custom factory, pool, and hook contracts is to utilize the patterns and methods established by the source code. ### 👨‍🔬 Testing Factories @@ -278,4 +278,8 @@ yarn test --match-contract ConstantSumPoolTest ### 🎣 Testing Hooks -- Coming soon™️ after update to 6th testnet deployment of v3 +The `VeBALFeeDiscountHookTest` mirrors the [VeBALFeeDiscountHookExampleTest](https://github.com/balancer/balancer-v3-monorepo/blob/main/pkg/pool-hooks/test/foundry/VeBALFeeDiscountHookExample.t.sol) + +``` +yarn test --match-contract VeBALFeeDiscountHookTest +``` diff --git a/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol b/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol index 6740c3f7..1f0e9c61 100644 --- a/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol +++ b/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol @@ -15,7 +15,7 @@ import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRou import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** - * @title VeBAL Fee Discount Hook + * @title VeBAL Fee Discount Hook Example */ contract VeBALFeeDiscountHook is BaseHooks { // only pools from the allowedFactory are able to register and use this hook @@ -54,8 +54,7 @@ contract VeBALFeeDiscountHook is BaseHooks { address, 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. + // If the router is not trusted, does not apply the veBAL discount because getSender() may be manipulated by a malicious router. if (params.router != _trustedRouter) { return (true, staticSwapFeePercentage); } diff --git a/packages/foundry/test/ConstantSumFactory.t.sol b/packages/foundry/test/ConstantSumFactory.t.sol index 1f880061..517fd2eb 100644 --- a/packages/foundry/test/ConstantSumFactory.t.sol +++ b/packages/foundry/test/ConstantSumFactory.t.sol @@ -73,9 +73,10 @@ contract ConstantSumFactoryTest is Test { function testPoolCreation__Fuzz(bytes32 salt) public { vm.assume(salt > 0); - ConstantSumPool pool = _createPool("Constant Sum Pool #1", "CSP1", tokenA, tokenB, bytes32(0)); + ConstantSumPool pool = _createPool("Constant Sum Pool #1", "CSP1", tokenA, tokenB, salt); assertEq(pool.name(), "Constant Sum Pool #1", "Wrong pool name"); assertEq(pool.symbol(), "CSP1", "Wrong pool symbol"); + assertEq(pool.decimals(), 18, "Wrong pool decimals"); } function testPoolSalt__Fuzz(bytes32 salt) public { diff --git a/packages/foundry/test/ConstantSumPool.t.sol b/packages/foundry/test/ConstantSumPool.t.sol index ee3d05e6..4cdb8314 100644 --- a/packages/foundry/test/ConstantSumPool.t.sol +++ b/packages/foundry/test/ConstantSumPool.t.sol @@ -19,9 +19,6 @@ import { ConstantSumPool } from "../contracts/pools/ConstantSumPool.sol"; import { ConstantSumFactory } from "../contracts/pools/ConstantSumFactory.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -/** - * @dev This test roughly mirrors how weighted pools are tested within BalancerV3 monorepo - */ contract ConstantSumPoolTest is BaseVaultTest { using ArrayHelpers for *; diff --git a/packages/foundry/test/VeBALFeeDiscountHook.t.sol b/packages/foundry/test/VeBALFeeDiscountHook.t.sol new file mode 100644 index 00000000..bbdce7a7 --- /dev/null +++ b/packages/foundry/test/VeBALFeeDiscountHook.t.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; +import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol"; +import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol"; +import { + HooksConfig, + LiquidityManagement, + PoolRoleAccounts, + TokenConfig +} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; + +import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/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 { VeBALFeeDiscountHook } from "../contracts/hooks/VeBALFeeDiscountHook.sol"; + +contract VeBALFeeDiscountHookTest is BaseVaultTest { + using FixedPoint for uint256; + using ArrayHelpers for *; + + uint256 internal daiIdx; + uint256 internal usdcIdx; + + address payable internal trustedRouter; + + function setUp() public override { + super.setUp(); + + (daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc)); + + // Grants to LP the ability to change static swap fee percentage + authorizer.grantRole(vault.getActionId(IVaultAdmin.setStaticSwapFeePercentage.selector), lp); + } + + 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 veBalFeeHook = address( + new VeBALFeeDiscountHook(IVault(address(vault)), address(factoryMock), address(veBAL), trustedRouter) + ); + vm.label(veBalFeeHook, "VeBAL Fee Hook"); + return veBalFeeHook; + } + + function testRegistryWithWrongFactory() public { + address veBalFeePool = _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, + veBalFeePool, + unauthorizedFactory + ) + ); + _registerPoolWithHook(veBalFeePool, tokenConfig, unauthorizedFactory); + } + + function testCreationWithWrongFactory() public { + address veBalFeePool = _createPoolToRegister(); + TokenConfig[] memory tokenConfig = vault.buildTokenConfig( + [address(dai), address(usdc)].toMemoryArray().asIERC20() + ); + + vm.expectRevert( + abi.encodeWithSelector( + IVaultErrors.HookRegistrationFailed.selector, + poolHooksContract, + veBalFeePool, + address(factoryMock) + ) + ); + _registerPoolWithHook(veBalFeePool, tokenConfig, address(factoryMock)); + } + + function testSuccessfulRegistry() public { + // Registering with allowed factory + address veBalFeePool = factoryMock.createPool("Test Pool", "TEST"); + TokenConfig[] memory tokenConfig = vault.buildTokenConfig( + [address(dai), address(usdc)].toMemoryArray().asIERC20() + ); + + _registerPoolWithHook(veBalFeePool, tokenConfig, address(factoryMock)); + + HooksConfig memory hooksConfig = vault.getHooksConfig(veBalFeePool); + + assertEq(hooksConfig.hooksContract, poolHooksContract, "Wrong poolHooksContract"); + assertEq(hooksConfig.shouldCallComputeDynamicSwapFee, true, "shouldCallComputeDynamicSwapFee is false"); + } + + function testSwapWithoutVeBal() public { + assertEq(veBAL.balanceOf(bob), 0, "Bob still has veBAL"); + + _doSwapAndCheckBalances(trustedRouter); + } + + function testSwapWithVeBal() public { + // Mint 1 veBAL to bob, so he's able to receive the fee discount + veBAL.mint(address(bob), 1); + assertGt(veBAL.balanceOf(bob), 0, "Bob does not have veBAL"); + + _doSwapAndCheckBalances(trustedRouter); + } + + function testSwapWithVeBalAndUntrustedRouter() public { + // Mint 1 veBAL to bob, so he's able to receive the fee discount + veBAL.mint(address(bob), 1); + assertGt(veBAL.balanceOf(bob), 0, "Bob does not have veBAL"); + + // Create an untrusted router + address payable untrustedRouter = payable(new RouterMock(IVault(address(vault)), weth, permit2)); + vm.label(untrustedRouter, "untrusted router"); + + // Allows permit2 to move DAI tokens from bob to untrustedRouter + vm.prank(bob); + permit2.approve(address(dai), untrustedRouter, type(uint160).max, type(uint48).max); + + // Even if bob has veBAL, since he is using an untrusted router, he will get no discounts + _doSwapAndCheckBalances(untrustedRouter); + } + + function _doSwapAndCheckBalances(address payable routerToUse) private { + // 10% swap fee. Since vault does not have swap fee, the fee will stay in the pool + uint256 swapFeePercentage = 1e17; + + vm.prank(lp); + vault.setStaticSwapFeePercentage(pool, swapFeePercentage); + + uint256 exactAmountIn = poolInitAmount / 100; + // PoolMock uses a linear math with rate 1, so amountIn = amountOut if no fees are applied + uint256 expectedAmountOut = exactAmountIn; + // If bob has veBAL and router is trusted, bob gets a 50% discount + bool shouldGetDiscount = routerToUse == trustedRouter && veBAL.balanceOf(bob) > 0; + uint256 expectedHookFee = exactAmountIn.mulDown(swapFeePercentage) / (shouldGetDiscount ? 2 : 1); + // Hook fee will remain in the pool, so the expected amount out discounts the fees + expectedAmountOut -= expectedHookFee; + + BaseVaultTest.Balances memory balancesBefore = getBalances(address(bob)); + + vm.prank(bob); + RouterMock(routerToUse).swapSingleTokenExactIn( + pool, + dai, + usdc, + exactAmountIn, + expectedAmountOut, + MAX_UINT256, + false, + bytes("") + ); + + BaseVaultTest.Balances memory balancesAfter = getBalances(address(bob)); + + // Bob's balance of DAI is supposed to decrease, since DAI is the token in + assertEq( + balancesBefore.userTokens[daiIdx] - balancesAfter.userTokens[daiIdx], + exactAmountIn, + "Bob's DAI balance is wrong" + ); + // Bob's balance of USDC is supposed to increase, since USDC is the token out + assertEq( + balancesAfter.userTokens[usdcIdx] - balancesBefore.userTokens[usdcIdx], + expectedAmountOut, + "Bob's USDC balance is wrong" + ); + + // Vault's balance of DAI is supposed to increase, since DAI was added by Bob + assertEq( + balancesAfter.vaultTokens[daiIdx] - balancesBefore.vaultTokens[daiIdx], + exactAmountIn, + "Vault's DAI balance is wrong" + ); + // Vault's balance of USDC is supposed to decrease, since USDC was given to Bob + assertEq( + balancesBefore.vaultTokens[usdcIdx] - balancesAfter.vaultTokens[usdcIdx], + expectedAmountOut, + "Vault's USDC balance is wrong" + ); + + // Pool deltas should equal vault's deltas + assertEq( + balancesAfter.poolTokens[daiIdx] - balancesBefore.poolTokens[daiIdx], + exactAmountIn, + "Pool's DAI balance is wrong" + ); + assertEq( + balancesBefore.poolTokens[usdcIdx] - balancesAfter.poolTokens[usdcIdx], + expectedAmountOut, + "Pool's USDC balance is wrong" + ); + } + + // Registry tests require a new pool, because an existing pool may be already registered + function _createPoolToRegister() private returns (address newPool) { + newPool = address(new PoolMock(IVault(address(vault)), "VeBAL Fee Pool", "veBALFeePool")); + vm.label(newPool, "VeBAL Fee Pool"); + } + + function _registerPoolWithHook(address exitFeePool, TokenConfig[] memory tokenConfig, address factory) private { + PoolRoleAccounts memory roleAccounts; + LiquidityManagement memory liquidityManagement; + + PoolFactoryMock(factory).registerPool( + exitFeePool, + tokenConfig, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } +}