Skip to content

Commit

Permalink
Improved deploy script pattern (#64)
Browse files Browse the repository at this point in the history
* Update deploy script patterns

* Improve comments and script names

* simplyify mock token contract comments

* Update deploy scripts to be more consistent and readable

* Update hooks page design

* Rename deploy scripts for clarity

* Update README with deploy script changes
  • Loading branch information
MattPereira authored Aug 15, 2024
1 parent ccad4ab commit 7abeb10
Show file tree
Hide file tree
Showing 17 changed files with 1,141 additions and 1,150 deletions.
82 changes: 35 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<!-- TODO: Record Updated Video -->
<!-- [![image](https://github.com/user-attachments/assets/2d0d5c6d-647d-4782-8d7a-9076b39319b9)](https://www.youtube.com/watch?v=2lInvpCt2o4) -->

### 📜 Requirements

Expand Down Expand Up @@ -144,7 +145,7 @@ const scaffoldConfig = {
</details>
## 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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down
57 changes: 37 additions & 20 deletions packages/foundry/contracts/hooks/VeBALFeeDiscountHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
11 changes: 2 additions & 9 deletions packages/foundry/contracts/mocks/MockToken1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
11 changes: 2 additions & 9 deletions packages/foundry/contracts/mocks/MockToken2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
11 changes: 2 additions & 9 deletions packages/foundry/contracts/mocks/MockVeBAL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 1 addition & 4 deletions packages/foundry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
Expand Down
34 changes: 34 additions & 0 deletions packages/foundry/script/00_DeployMockTokens.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit 7abeb10

Please sign in to comment.