Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hooks example test #63

Merged
merged 1 commit into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,22 +206,22 @@ 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
- Set any hooks contracts constructor args

#### `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
Expand Down Expand Up @@ -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

Expand All @@ -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
```
5 changes: 2 additions & 3 deletions packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/foundry/test/ConstantSumFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 0 additions & 3 deletions packages/foundry/test/ConstantSumPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 *;

Expand Down
231 changes: 231 additions & 0 deletions packages/foundry/test/VeBALFeeDiscountHook.t.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Loading