diff --git a/README.md b/README.md
index 84d02c85..849b02e9 100644
--- a/README.md
+++ b/README.md
@@ -15,16 +15,17 @@ A full stack prototyping tool for building on top of Balancer v3. Accelerate the
### ๐ชง Table Of Contents
-0. [Environment Setup ๐งโ๐ป](#0-environment-setup-)
-1. [Create a Custom Pool ๐](#1-create-a-custom-pool-)
-2. [Create a Pool Factory ๐ญ](#2-create-a-pool-factory-)
-3. [Create a Pool Hook ๐ช](#3-create-a-pool-hook-)
-4. [Deploy the Contracts ๐ข](#4-deploy-the-contracts-)
-5. [Test the Contracts ๐งช](#5-test-the-contracts-)
+- [๐งโ๐ป Environment Setup](#0-environment-setup-)
+- [๐ Create a Custom Pool](#1-create-a-custom-pool-)
+- [๐ญ Create a Pool Factory](#2-create-a-pool-factory-)
+- [๐ช Create a Pool Hook](#3-create-a-pool-hook-)
+- [๐ข Deploy the Contracts](#4-deploy-the-contracts-)
+- [๐งช Test the Contracts](#5-test-the-contracts-)
-## 0. Environment Setup ๐งโ๐ป
+## ๐งโ๐ป Environment Setup
-[![image](https://github.com/user-attachments/assets/2d0d5c6d-647d-4782-8d7a-9076b39319b9)](https://www.youtube.com/watch?v=2lInvpCt2o4)
+
+
### ๐ Requirements
@@ -144,7 +145,7 @@ const scaffoldConfig = {
-## 1. Create a Custom Pool ๐
+## ๐ Create a Custom Pool
Your journey begins with planning the custom computation logic for the pool, which defines how an AMM exchanges one asset for another.
@@ -162,7 +163,7 @@ Your journey begins with planning the custom computation logic for the pool, whi
- To get started, edit the`ConstantSumPool.sol` contract directly or make a copy
-## 2. Create a Pool Factory ๐ญ
+## ๐ญ Create a Pool Factory
After designing a pool contract, the next step is to prepare a factory contract because Balancer's off-chain infrastructure uses the factory address as a means to identify the type of pool, which is important for integration into the UI, SDK, and external aggregators
@@ -180,7 +181,7 @@ After designing a pool contract, the next step is to prepare a factory contract
- To get started, edit the`ConstantSumFactory.sol` contract directly or make a copy
-## 3. Create a Pool Hook ๐ช
+## ๐ช Create a Pool Hook
Next, consider further extending the functionality of the custom pool contract with a hooks contract. If your custom pool does not need a hooks contract, use the zero address during pool registration
@@ -198,33 +199,39 @@ Next, consider further extending the functionality of the custom pool contract w
- To get started, edit the `VeBALFeeDiscountHook.sol` contract directly or make a copy
-## 4. Deploy the Contracts ๐ข
+## ๐ข Deploy the Contracts
-The deploy scripts are all located in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory and are prefixed with a number based on the order the order they're intended to be run. The mock tokens, factories, and hooks contracts must be deployed before the pools. On the frontend, the [Pools](http://localhost:3000/pools) page will automatically add a button above the search bar for any pools deployed using the latest factory contract
+The deploy scripts are all located in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory. All deploy scripts should be run inside of `Deploy.s.sol` so that the `export` modifier can automate the transfer of deployed contract info to `nextjs/contracts/depoloyedContracts.ts`
-### ๐ ๏ธ Adjust the Deploy Scripts
+### ๐ฏ Follow the Pattern
-#### `00_DeploySetup.s.sol`
+To add a new deploy script, import it into `Deploy.s.sol`, create a new instance of it, and run it
-Deploy mock tokens, factory contracts, and hooks contracts to be used by pools
+```
+function run() external virtual export {
+ DeployYourContract deployYourContract = new DeployYourContract();
+ deployYourContract.run();
+}
+```
-- Set the `pauseWindowDuration` for the factory contracts
-- Set the mock token names, symbols, and supply
-- Set any hooks contracts constructor args
+### ๐ ๏ธ Examine the Example Deploy Scripts
-#### `01_DeployConstantSumPool.s.sol`
+#### `00_DeployMockTokens.s.sol`
-Deploy, register, and initialize a Constant Sum Pool
+1. Deploys mock tokens that are used to register and initialize pools
+2. Deploys a mock token used for the example `VeBALFeeDiscountHook` contract
-- Set the pool registration config in the `getRegistrationConfig()` function
-- Set the pool initialization config in the `getInitializationConfig()` function
+#### `01_DeployConstantSum.s.sol`
-#### `02_DeployConstantProductPool.s.sol`
+1. Deploys a `ConstantSumFactory`
+2. Deploys and registers a `ConstantSumPool`
+3. Initializes the `ConstantSumPool` using mock tokens
-Deploy, register, and initialize a Constant Product Pool
+#### `02_DeployConstantProduct.s.sol`
-- Set the pool registration config in the `getRegistrationConfig()` function
-- Set the pool initialization config in the `getInitializationConfig()` function
+1. Deploys a `ConstantProductFactory`
+2. Deploys and registers a `ConstantProductPool`
+3. Initializes the `ConstantProductPool` using mock tokens
### ๐ก Broadcast the Transactions
@@ -234,28 +241,9 @@ To run all the deploy scripts
yarn deploy
```
-To run only the `DeploySetup` script
-
-```bash
-yarn deploy:setup
-```
-
-To run only the `DeployConstantSumPool` script
-
-```bash
-yarn deploy:sum
-```
-
-To run only the `DeployConstantProductPool` script
-
-```bash
-yarn deploy:product
-```
-
๐ To deploy to the live sepolia testnet, add the `--network sepolia` flag
-๐ To modify the yarn commands, edit the "scripts" section of the [/foundry/package.json](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/package.json)
-## 5. Test the Contracts ๐งช
+## ๐งช Test the Contracts
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.
diff --git a/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol b/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol
index 1f0e9c61..3ce87035 100644
--- a/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol
+++ b/packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol
@@ -9,63 +9,80 @@ 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";
/**
- * @title VeBAL Fee Discount Hook Example
+ * @title VeBAL Fee Discount Hook
+ * @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/contracts/mocks/MockToken1.sol b/packages/foundry/contracts/mocks/MockToken1.sol
index ed3776c2..9bf86226 100644
--- a/packages/foundry/contracts/mocks/MockToken1.sol
+++ b/packages/foundry/contracts/mocks/MockToken1.sol
@@ -3,20 +3,13 @@ pragma solidity ^0.8.24;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
-/**
- * @title Mock Token 1
- * @notice Entire initial supply is minted to the deployer
- * @dev Default decimals is 18, but you can override the decimals function from ERC20
- */
contract MockToken1 is ERC20 {
+ // Mint the initial supply to the deployer
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
- /**
- * Allow any user to mint any amount of tokens to their wallet
- * This function is accessible on the frontend's "Debug" page
- */
+ // Allow any user to mint any amount of tokens to their wallet
function mint(uint256 amount) external {
_mint(msg.sender, amount);
}
diff --git a/packages/foundry/contracts/mocks/MockToken2.sol b/packages/foundry/contracts/mocks/MockToken2.sol
index 1264bcf3..3474d914 100644
--- a/packages/foundry/contracts/mocks/MockToken2.sol
+++ b/packages/foundry/contracts/mocks/MockToken2.sol
@@ -3,20 +3,13 @@ pragma solidity ^0.8.24;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
-/**
- * @title Mock Token 2
- * @notice Entire initial supply is minted to the deployer
- * @dev Default decimals is 18, but you can override the decimals function from ERC20
- */
contract MockToken2 is ERC20 {
+ // Mint the initial supply to the deployer
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
- /**
- * Allow any user to mint any amount of tokens to their wallet
- * This function is accessible on the frontend's "Debug" page
- */
+ // Allow any user to mint any amount of tokens to their wallet
function mint(uint256 amount) external {
_mint(msg.sender, amount);
}
diff --git a/packages/foundry/contracts/mocks/MockVeBAL.sol b/packages/foundry/contracts/mocks/MockVeBAL.sol
index 832666ee..107a5bd8 100644
--- a/packages/foundry/contracts/mocks/MockVeBAL.sol
+++ b/packages/foundry/contracts/mocks/MockVeBAL.sol
@@ -3,20 +3,13 @@ pragma solidity ^0.8.24;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
-/**
- * @title Mock VeBAL
- * @notice Entire initial supply is minted to the deployer
- * @dev Default decimals is 18, but you can override the decimals function from ERC20
- */
contract MockVeBAL is ERC20 {
+ // Mint the initial supply to the deployer
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
- /**
- * Allow any user to mint any amount of tokens to their wallet
- * This function is accessible on the frontend's "Debug" page
- */
+ // Allow any user to mint any amount of tokens to their wallet
function mint(uint256 amount) external {
_mint(msg.sender, amount);
}
diff --git a/packages/foundry/package.json b/packages/foundry/package.json
index 5d6478b3..c98d1913 100644
--- a/packages/foundry/package.json
+++ b/packages/foundry/package.json
@@ -5,10 +5,7 @@
"account": "node script/ListAccount.js",
"chain": "anvil --config-out localhost.json",
"compile": "forge compile",
- "deploy": "yarn deploy:setup && yarn deploy:sum && yarn deploy:product",
- "deploy:setup": "forge build --build-info --build-info-path out/build-info/ && forge script script/00_DeploySetup.s.sol --rpc-url ${1:-default_network} --broadcast --legacy && node scripts-js/generateTsAbis.js",
- "deploy:sum": "forge script script/01_DeployConstantSumPool.s.sol --rpc-url ${1:-default_network} --broadcast",
- "deploy:product": "forge script script/02_DeployConstantProductPool.s.sol --rpc-url ${1:-default_network} --broadcast",
+ "deploy": "forge build --build-info --build-info-path out/build-info/ && forge script script/Deploy.s.sol --rpc-url ${1:-default_network} --broadcast --legacy && node scripts-js/generateTsAbis.js",
"flatten": "forge flatten",
"fork": "anvil --fork-url ${0:-sepolia} --chain-id 31337 --config-out localhost.json",
"format": "npx prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol' 'test/**/*.sol' 'script/*.sol' 'utils/*.sol'",
diff --git a/packages/foundry/script/00_DeployMockTokens.s.sol b/packages/foundry/script/00_DeployMockTokens.s.sol
new file mode 100644
index 00000000..dfebe5c8
--- /dev/null
+++ b/packages/foundry/script/00_DeployMockTokens.s.sol
@@ -0,0 +1,34 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
+import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
+
+import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol";
+import { MockToken1 } from "../contracts/mocks/MockToken1.sol";
+import { MockToken2 } from "../contracts/mocks/MockToken2.sol";
+import { MockVeBAL } from "../contracts/mocks/MockVeBAL.sol";
+
+/**
+ * @title Deploy Mock Tokens
+ * @notice Deploys mock tokens for use with pools and hooks
+ */
+contract DeployMockTokens is ScaffoldHelpers {
+ function run() external virtual returns (IERC20 mockToken1, IERC20 mockToken2, IERC20 mockVeBAL) {
+ uint256 deployerPrivateKey = getDeployerPrivateKey();
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ // For use with pool contracts
+ mockToken1 = new MockToken1("Mock Token 1", "MT1", 1000e18);
+ mockToken2 = new MockToken2("Mock Token 2", "MT2", 1000e18);
+ console.log("MockToken1 deployed at: %s", address(mockToken1));
+ console.log("MockToken2 deployed at: %s", address(mockToken2));
+
+ // For use with VeBALFeeDiscountHook
+ mockVeBAL = new MockVeBAL("Vote-escrow BAL", "veBAL", 1000e18);
+ console.log("Mock Vote-escrow BAL deployed at: %s", address(mockVeBAL));
+
+ vm.stopBroadcast();
+ }
+}
diff --git a/packages/foundry/script/00_DeploySetup.s.sol b/packages/foundry/script/00_DeploySetup.s.sol
deleted file mode 100644
index 44a8b413..00000000
--- a/packages/foundry/script/00_DeploySetup.s.sol
+++ /dev/null
@@ -1,62 +0,0 @@
-//SPDX-License-Identifier: MIT
-pragma solidity ^0.8.24;
-
-import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
-import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
-
-import { PoolHelpers } from "./PoolHelpers.sol";
-import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol";
-import { ConstantSumFactory } from "../contracts/pools/ConstantSumFactory.sol";
-import { ConstantProductFactory } from "../contracts/pools/ConstantProductFactory.sol";
-import { VeBALFeeDiscountHook } from "../contracts/hooks/VeBALFeeDiscountHook.sol";
-import { MockToken1 } from "../contracts/mocks/MockToken1.sol";
-import { MockToken2 } from "../contracts/mocks/MockToken2.sol";
-import { MockVeBAL } from "../contracts/mocks/MockVeBAL.sol";
-
-/**
- * @title Deploy Setup
- * @notice Deploys mock tokens, factory contracts, and hooks contracts to be used by the pools
- * @dev This script runs as part of `yarn deploy`, but can also be run discretely with `yarn deploy:setup`
- * @dev Set the pauseWindowDuration for the factory contracts below
- */
-contract DeploySetup is PoolHelpers, ScaffoldHelpers {
- function run() external virtual {
- uint256 deployerPrivateKey = getDeployerPrivateKey();
-
- uint32 pauseWindowDuration = 365 days; // The period during which pools can be paused and unpaused ( starting from deployment of the factory )
-
- vm.startBroadcast(deployerPrivateKey);
-
- // Deploy the factory contracts
- ConstantSumFactory sumFactory = new ConstantSumFactory(IVault(vault), pauseWindowDuration);
- ConstantProductFactory productFactory = new ConstantProductFactory(IVault(vault), pauseWindowDuration);
- console.log("Constant Sum Factory deployed at: %s", address(sumFactory));
- console.log("Constant Product Factory deployed at: %s", address(productFactory));
-
- // Deploy mock tokens
- IERC20 token1 = new MockToken1("Mock Token 1", "MT1", 1000e18);
- IERC20 token2 = new MockToken2("Mock Token 2", "MT2", 1000e18);
- IERC20 veBAL = new MockVeBAL("Vote-escrow BAL", "veBAL", 1000e18);
- console.log("MockToken1 deployed at: %s", address(token1));
- console.log("MockToken2 deployed at: %s", address(token2));
- console.log("Mock Vote-escrow BAL deployed at: %s", address(veBAL));
-
- // Deploy a hooks contract for the Constant Product Factory
- VeBALFeeDiscountHook poolHooksContract = new VeBALFeeDiscountHook(
- IVault(vault),
- address(productFactory),
- address(veBAL),
- address(router)
- );
- console.log("VeBALFeeDiscountHook deployed at address: %s", address(poolHooksContract));
-
- vm.stopBroadcast();
-
- /**
- * This function generates the file containing the contracts Abi definitions that are carried from /foundry to /nextjs.
- * These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example.
- * This function should be called last.
- */
- exportDeployments();
- }
-}
diff --git a/packages/foundry/script/01_DeployConstantSumPool.s.sol b/packages/foundry/script/01_DeployConstantSum.s.sol
similarity index 74%
rename from packages/foundry/script/01_DeployConstantSumPool.s.sol
rename to packages/foundry/script/01_DeployConstantSum.s.sol
index 7dfa8604..559d914f 100644
--- a/packages/foundry/script/01_DeployConstantSumPool.s.sol
+++ b/packages/foundry/script/01_DeployConstantSum.s.sol
@@ -10,42 +10,32 @@ 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 { DevOpsTools } from "lib/foundry-devops/src/DevOpsTools.sol";
import { PoolHelpers } from "./PoolHelpers.sol";
import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol";
import { ConstantSumFactory } from "../contracts/pools/ConstantSumFactory.sol";
/**
- * @title Deploy Constant Sum Pool
+ * @title Deploy Constant Sum
* @notice Deploys, registers, and initializes a Constant Sum Pool
- * @dev Set the registration & initialization configurations in the internal getter functions below
- * @dev This script runs as part of `yarn deploy`, but can also be run discretely with `yarn deploy:sum`
*/
-contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
- function run() external virtual {
+contract DeployConstantSum is PoolHelpers, ScaffoldHelpers {
+ function run(IERC20 token1, IERC20 token2) external virtual {
+ // Set the deployment configurations
+ uint32 pauseWindowDuration = 365 days;
+ PoolRegistrationConfig memory regConfig = getPoolRegistrationConfig(token1, token2);
+ PoolInitializationConfig memory initConfig = getPoolInitializationConfig(token1, token2);
+
+ // Start creating the transactions
uint256 deployerPrivateKey = getDeployerPrivateKey();
+ vm.startBroadcast(deployerPrivateKey);
- // Grab the latest deployment addresses for the mock tokens and constant sum factory
- address token1 = DevOpsTools.get_most_recent_deployment(
- "MockToken1", // Must match the mock token contract name
- block.chainid
- );
- address token2 = DevOpsTools.get_most_recent_deployment(
- "MockToken2", // Must match the mock token contract name
- block.chainid
- );
- address factory = DevOpsTools.get_most_recent_deployment(
- "ConstantSumFactory", // Must match the factory contract name
- block.chainid
- );
- // Grab arguments for pool deployment and initialization outside of broadcast to save gas
- RegistrationConfig memory regConfig = getRegistrationConfig(token1, token2);
- InitializationConfig memory initConfig = getInitializationConfig(token1, token2);
+ // Deploy a constant sum factory contract
+ ConstantSumFactory factory = new ConstantSumFactory(vault, pauseWindowDuration);
+ console.log("Constant Sum Factory deployed at: %s", address(factory));
- vm.startBroadcast(deployerPrivateKey);
// Deploy a pool and register it with the vault
- address pool = ConstantSumFactory(factory).create(
+ address pool = factory.create(
regConfig.name,
regConfig.symbol,
regConfig.salt,
@@ -58,8 +48,14 @@ contract DeployConstantSumPool 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,
@@ -77,10 +73,10 @@ contract DeployConstantSumPool 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(
- address token1,
- address token2
- ) internal view returns (RegistrationConfig memory regConfig) {
+ function getPoolRegistrationConfig(
+ IERC20 token1,
+ IERC20 token2
+ ) 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
@@ -90,13 +86,13 @@ contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
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: IERC20(token1),
+ token: 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
});
tokenConfig[1] = TokenConfig({ // Make sure to have proper token order (alphanumeric)
- token: IERC20(token2),
+ token: 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
@@ -114,7 +110,7 @@ contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
enableDonation: false
});
- regConfig = RegistrationConfig({
+ config = PoolRegistrationConfig({
name: name,
symbol: symbol,
salt: salt,
@@ -131,13 +127,13 @@ contract DeployConstantSumPool 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(
- address token1,
- address token2
- ) internal pure returns (InitializationConfig memory poolInitConfig) {
+ function getPoolInitializationConfig(
+ IERC20 token1,
+ IERC20 token2
+ ) internal pure returns (PoolInitializationConfig 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);
+ tokens[0] = token1;
+ tokens[1] = 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
@@ -145,7 +141,7 @@ contract DeployConstantSumPool 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/02_DeployConstantProductPool.s.sol b/packages/foundry/script/02_DeployConstantProduct.s.sol
similarity index 71%
rename from packages/foundry/script/02_DeployConstantProductPool.s.sol
rename to packages/foundry/script/02_DeployConstantProduct.s.sol
index 6ba3de8f..c4908915 100644
--- a/packages/foundry/script/02_DeployConstantProductPool.s.sol
+++ b/packages/foundry/script/02_DeployConstantProduct.s.sol
@@ -11,46 +11,42 @@ import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/vault/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 { DevOpsTools } from "lib/foundry-devops/src/DevOpsTools.sol";
import { PoolHelpers } from "./PoolHelpers.sol";
import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol";
+import { VeBALFeeDiscountHook } from "../contracts/hooks/VeBALFeeDiscountHook.sol";
import { ConstantProductFactory } from "../contracts/pools/ConstantProductFactory.sol";
/**
- * @title Deploy Constant Product Pool
- * @notice Deploys, registers, and initializes a Constant Product Pool
- * @dev Set the registration & initialization configurations in the internal getter functions
- * @dev This script runs as part of `yarn deploy`, but can also be run discretely with `yarn deploy:product`
+ * @title Deploy Constant Product
+ * @notice Deploys a factory and hooks contract and then deploys, registers, and initializes a constant product pool
*/
-contract DeployConstantProductPool is PoolHelpers, ScaffoldHelpers {
- function run() external virtual {
+contract DeployConstantProduct is PoolHelpers, ScaffoldHelpers {
+ function run(IERC20 token1, IERC20 token2, IERC20 veBAL) external virtual {
+ // Set the deployment configurations
+ uint32 pauseWindowDuration = 365 days;
+ PoolRegistrationConfig memory regConfig = getPoolRegistrationConfig(token1, token2);
+ PoolInitializationConfig memory initConfig = getPoolInitializationConfig(token1, token2);
+
+ // Start creating the transactions
uint256 deployerPrivateKey = getDeployerPrivateKey();
+ vm.startBroadcast(deployerPrivateKey);
- // Grab the latest deployment addresses for the mock tokens, constant product factory, and hooks contract
- address token1 = DevOpsTools.get_most_recent_deployment(
- "MockToken1", // Must match the mock token contract name
- block.chainid
- );
- address token2 = DevOpsTools.get_most_recent_deployment(
- "MockToken2", // Must match the mock token contract name
- block.chainid
- );
- address factory = DevOpsTools.get_most_recent_deployment(
- "ConstantProductFactory", // Must match the mock token contract name
- block.chainid
- );
- address poolHooksContract = DevOpsTools.get_most_recent_deployment(
- "VeBALFeeDiscountHook", // Must match the hooks contract name
- block.chainid
+ // Deploy a constant sum factory contract
+ ConstantProductFactory factory = new ConstantProductFactory(IVault(vault), pauseWindowDuration);
+ console.log("Constant Product Factory deployed at: %s", address(factory));
+
+ // Deploy a hooks contract
+ VeBALFeeDiscountHook poolHooksContract = new VeBALFeeDiscountHook(
+ IVault(vault),
+ address(factory),
+ address(router),
+ IERC20(veBAL)
);
- // Grab arguments for pool deployment and initialization outside of broadcast to save gas
- RegistrationConfig memory regConfig = getRegistrationConfig(IERC20(token1), IERC20(token2));
- InitializationConfig memory initConfig = getInitializationConfig(IERC20(token1), IERC20(token2));
+ console.log("VeBALFeeDiscountHook deployed at address: %s", address(poolHooksContract));
- vm.startBroadcast(deployerPrivateKey);
// Deploy a pool and register it with the vault
- address pool = ConstantProductFactory(factory).create(
+ address pool = factory.create(
regConfig.name,
regConfig.symbol,
regConfig.salt,
@@ -58,13 +54,19 @@ contract DeployConstantProductPool is PoolHelpers, ScaffoldHelpers {
regConfig.swapFeePercentage,
regConfig.protocolFeeExempt,
regConfig.roleAccounts,
- poolHooksContract,
+ address(poolHooksContract),
regConfig.liquidityManagement
);
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,
@@ -78,15 +80,14 @@ contract DeployConstantProductPool 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 getRegistrationConfig(
+ 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
@@ -94,7 +95,7 @@ contract DeployConstantProductPool 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
@@ -120,7 +121,7 @@ contract DeployConstantProductPool is PoolHelpers, ScaffoldHelpers {
enableDonation: false
});
- regConfig = RegistrationConfig({
+ config = PoolRegistrationConfig({
name: name,
symbol: symbol,
salt: salt,
@@ -137,10 +138,10 @@ contract DeployConstantProductPool is PoolHelpers, ScaffoldHelpers {
* @dev Set the pool initialization configurations here
* @notice This is where the amounts of tokens to seed the pool with initial liquidity 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;
@@ -151,7 +152,7 @@ contract DeployConstantProductPool 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/Deploy.s.sol b/packages/foundry/script/Deploy.s.sol
new file mode 100644
index 00000000..c66434ce
--- /dev/null
+++ b/packages/foundry/script/Deploy.s.sol
@@ -0,0 +1,34 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+import { ScaffoldHelpers } from "./ScaffoldHelpers.sol";
+import { DeployMockTokens } from "./00_DeployMockTokens.s.sol";
+import { DeployConstantSum } from "./01_DeployConstantSum.s.sol";
+import { DeployConstantProduct } from "./02_DeployConstantProduct.s.sol";
+import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
+
+/**
+ * @title Deploy Script
+ * @dev Import & run deploy scripts here so that contract Abis are carried to /nextjs
+ * @dev Run this script with `yarn deploy`
+ */
+contract DeployScript is ScaffoldHelpers {
+ function run() external virtual export {
+ // Deploy mock tokens to be used for pools and hooks contracts
+ DeployMockTokens deployMockTokens = new DeployMockTokens();
+ (IERC20 mockToken1, IERC20 mockToken2, IERC20 mockVeBAL) = deployMockTokens.run();
+
+ // Deploy a constant sum factory and a pool
+ DeployConstantSum deployConstantSum = new DeployConstantSum();
+ deployConstantSum.run(mockToken1, mockToken2);
+
+ // Deploy a constant product factory, a hooks contract, and a pool
+ DeployConstantProduct deployConstantProduct = new DeployConstantProduct();
+ deployConstantProduct.run(mockToken1, mockToken2, mockVeBAL);
+ }
+
+ modifier export() {
+ _;
+ exportDeployments();
+ }
+}
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/scripts-js/generateTsAbis.js b/packages/foundry/scripts-js/generateTsAbis.js
index c05c8873..4553e35a 100644
--- a/packages/foundry/scripts-js/generateTsAbis.js
+++ b/packages/foundry/scripts-js/generateTsAbis.js
@@ -60,7 +60,7 @@ function getInheritedFunctions(mainArtifact) {
}
function main() {
- const current_path_to_broadcast = path.join(__dirname, '..', 'broadcast/00_DeploySetup.s.sol');
+ const current_path_to_broadcast = path.join(__dirname, '..', 'broadcast/Deploy.s.sol');
console.log('current_path_to_broadcast', current_path_to_broadcast);
const current_path_to_deployments = path.join(__dirname, '..', 'deployments');
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/app/hooks/_components/HooksCards.tsx b/packages/nextjs/app/hooks/_components/HooksCards.tsx
index 6ddcfda2..55920dd3 100644
--- a/packages/nextjs/app/hooks/_components/HooksCards.tsx
+++ b/packages/nextjs/app/hooks/_components/HooksCards.tsx
@@ -36,26 +36,26 @@ export const HooksCards = ({ hooks }: { hooks: HookDetails[] }) => {
return (
<>
-