Skip to content

Commit

Permalink
Improvements made during recording of intro to scaffold (#75)
Browse files Browse the repository at this point in the history
* Refactor main deploy script to use inheritance

* Update README.md

* Update README.md

* Update README.md

* update header & home page title

* Update README.md
  • Loading branch information
MattPereira authored Sep 8, 2024
1 parent 9a66a5b commit ae8ca5e
Show file tree
Hide file tree
Showing 17 changed files with 379 additions and 382 deletions.
72 changes: 26 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
# Scaffold Balancer v3

A full stack prototyping tool for building on top of Balancer v3. Accelerate the process of designing and deploying custom pools and hooks contracts. Concentrate on mastering the core concepts within a swift and responsive environment augmented by a local fork and a frontend pool operations playground.
A prototyping tool and starter kit for building on top of Balancer v3. Accelerate the process of designing and deploying custom pools and hooks contracts. Concentrate on mastering the core concepts within a swift and responsive environment augmented by a local fork and a frontend pool operations playground.

### 🛠️ Tech Stack

| [Balancer SDK](https://github.com/balancer/b-sdk) | [Scaffold ETH 2](https://scaffold-eth-2-docs.vercel.app/) | [Balancer v3 Monorepo](https://github.com/balancer/balancer-v3-monorepo) |
| ------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------ |
![v3-architecture](https://github.com/user-attachments/assets/584c2a44-5382-4abe-905d-507fb55f5f25)

### 📚 Prerequisites

- Basic understanding of [Solidity](https://docs.soliditylang.org/) and [Foundry](https://book.getfoundry.sh/)
- Basic understanding of [liquidity pools](https://www.youtube.com/watch?v=cizLhxSKrAc) and [AMMs](https://chain.link/education-hub/what-is-an-automated-market-maker-amm)
- Basic understanding of [Balancer v3](https://docs-v3.balancer.fi/concepts/core-concepts/introduction.html)

### 🪧 Table Of Contents

- [🧑‍💻 Environment Setup](#-environment-setup)
- [🕵️ Explore the Examples](#-explore-the-examples)
- [🌊 Create a Custom Pool](#-create-a-custom-pool)
- [🏭 Create a Pool Factory](#-create-a-pool-factory)
- [🪝 Create a Pool Hook](#-create-a-pool-hook)
Expand Down Expand Up @@ -145,6 +139,24 @@ const scaffoldConfig = {
</details>
## 🕵️ Explore the Examples
### 1. Constant Sum Pool with Dynamic Swap Fee Hook
The swap fee percentage is altered by the hook contract before the pool calculates the amount for the swap
![dynamic-fee-hook](https://github.com/user-attachments/assets/63ab25c2-a530-4bb9-9946-e8cebcb5ab9d)
### 2. Constant Product Pool with Lottery Hook
After the pool calculates the amount for the swap, an after swap hook makes a request to an oracle contract for a random number
![after-swap-hook](https://github.com/user-attachments/assets/39822cf0-1053-4a66-b303-acf63542fcdd)
### 3. Weighted Pool with Exit Fee Hook
After the pool calculates the amounts of tokens for an exit operation, an after remove liquidity hook adjusts the amounts before the vault transfers tokens to the user
![after-remove-liquidity-hook](https://github.com/user-attachments/assets/ca6003ba-7e0c-4431-a7ef-b3273f170c62)
## 🌊 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 @@ -155,7 +167,6 @@ Your journey begins with planning the custom computation logic for the pool, whi
### 1. Review the Docs 📖
- [Create a custom AMM with a novel invariant](https://docs-v3.balancer.fi/build-a-custom-amm/build-an-amm/create-custom-amm-with-novel-invariant.html)
- [Creata a ]
### 2. Recall the Key Requirements 🔑
Expand Down Expand Up @@ -207,47 +218,16 @@ Next, consider further extending the functionality of the custom pool contract w
## 🚢 Deploy the Contracts
The deploy scripts are located in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory.
The deploy scripts are located in the [foundry/script/](https://github.com/balancer/scaffold-balancer-v3/tree/main/packages/foundry/script) directory. To better understand the lifecycle of deploying a pool that uses a hooks contract, see the diagram below
### 1. Examine the Deploy Scripts 🕵️
![pool-deploy-scripts](https://github.com/user-attachments/assets/3733296c-9c64-40c8-8139-f2878e6379c4)
#### [00_DeployMockTokens.s.sol](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/script/00_DeployMockTokens.s.sol)
1. Deploys mock tokens that are used to register and initialize pools
2. Deploys a mock token used for the example `VeBALFeeDiscountHook` contract
### 1. Modifying the Deploy Scripts 🛠️
#### [01_DeployConstantSumPool.s.sol](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/script/01_DeployConstantSumPool.s.sol)
1. Deploys a `ConstantSumFactory`
2. Deploys and registers a `ConstantSumPool`
3. Initializes the `ConstantSumPool` using mock tokens
#### [02_DeployConstantProductPool.s.sol](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/script/02_DeployConstantProductPool.s.sol)
1. Deploys a `ConstantProductFactory`
2. Deploys a `VeBALFeeDiscountHook` that can only be used by pools created by the `ConstantProductFactory`
3. Deploys and registers a `ConstantProductPool`
4. Initializes the `ConstantProductPool` using mock tokens
#### [03_DeployWeightedPool8020.s.sol](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/script/03_DeployWeightedPool8020.s.sol)
1. Deploys a `WeightedPoolFactory`
2. Deploys an `ExitFeeHook`
3. Deploys and registers a `WeightedPool` with 80/20 weights
4. Initializes the `WeightedPool` using mock tokens
### 2. Modify the Deploy Scripts 🛠️
Each deploy scripts should be run inside of `Deploy.s.sol` so that the `scaffoldExport` modifier can automate the transfer of deployed contract info to the nextjs front-end. To add a new deploy script, import it into `Deploy.s.sol`, create a new instance of it, and run it
```
DeployYourContract deployYourContract = new DeployYourContract();
deployYourContract.run();
```
For all the scaffold integrations to work properly, each deploy script must be imported into `Deploy.s.sol` and inherited by the `DeployScript` contract in `Deploy.s.sol`
### 3. Broadcast the Transactions 📡
### 2. Broadcast the Transactions 📡
To run all the deploy scripts
Expand Down
17 changes: 8 additions & 9 deletions packages/foundry/contracts/hooks/ExitFeeHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {

import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";
import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol";
import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol";

/**
* @notice Impose an "exit fee" on a pool. The value of the fee is returned to the LPs.
Expand All @@ -41,6 +42,8 @@ contract ExitFeeHook is BaseHooks, Ownable {
// Percentages are represented as 18-decimal FP numbers, which have a maximum value of FixedPoint.ONE (100%),
// so 60 bits are sufficient.
uint64 public exitFeePercentage;
// Only pools deployed by the allowed factory may register
address private immutable _allowedFactory;

// Maximum exit fee of 10%
uint64 public constant MAX_EXIT_FEE_PERCENTAGE = 10e16;
Expand All @@ -59,27 +62,23 @@ contract ExitFeeHook is BaseHooks, Ownable {
*/
error PoolDoesNotSupportDonation();

constructor(IVault vault) BaseHooks(vault) Ownable(msg.sender) {
// solhint-disable-previous-line no-empty-blocks
constructor(IVault vault, address allowedFactory) BaseHooks(vault) Ownable(msg.sender) {
_allowedFactory = allowedFactory;
}

/// @inheritdoc IHooks
function onRegister(
address,
address,
address factory,
address pool,
TokenConfig[] memory,
LiquidityManagement calldata liquidityManagement
) public view override onlyVault returns (bool) {
// NOTICE: In real hooks, make sure this function is properly implemented (e.g. check the factory, and check
// that the given pool is from the factory). Returning true unconditionally allows any pool, with any
// configuration, to use this hook.

// This hook requires donation support to work (see above).
if (liquidityManagement.enableDonation == false) {
revert PoolDoesNotSupportDonation();
}

return true;
return factory == _allowedFactory && IBasePoolFactory(factory).isPoolFromFactory(pool);
}

/// @inheritdoc IHooks
Expand Down
16 changes: 9 additions & 7 deletions packages/foundry/contracts/hooks/LotteryHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TokenConfig,
HookFlags
} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol";

import { EnumerableMap } from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/EnumerableMap.sol";
import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";
Expand All @@ -35,6 +36,8 @@ contract LotteryHook is BaseHooks, Ownable {

// Trusted router is needed since we rely on `getSender` to know which user should receive the prize.
address private immutable _trustedRouter;
// Only pools deployed by the allowed factory may register
address private immutable _allowedFactory;

// When calling `onAfterSwap`, a random number is generated. If the number is equal to LUCKY_NUMBER, the user will
// win the accrued fees. It must be a number between 1 and MAX_NUMBER, or else nobody will win.
Expand All @@ -51,21 +54,20 @@ contract LotteryHook is BaseHooks, Ownable {

uint256 private _counter = 0;

constructor(IVault vault, address router) BaseHooks(vault) Ownable(msg.sender) {
constructor(IVault vault, address allowedFactory, address router) BaseHooks(vault) Ownable(msg.sender) {
_allowedFactory = allowedFactory;
_trustedRouter = router;
}

/// @inheritdoc IHooks
function onRegister(
address,
address,
address factory,
address pool,
TokenConfig[] memory,
LiquidityManagement calldata
) public view override onlyVault returns (bool) {
// NOTICE: In real hooks, make sure this function is properly implemented (e.g. check the factory, and check
// that the given pool is from the factory). Returning true unconditionally allows any pool, with any
// configuration, to use this hook.
return true;
// Only pools deployed by an allowed factory may register
return factory == _allowedFactory && IBasePoolFactory(factory).isPoolFromFactory(pool);
}

/// @inheritdoc IHooks
Expand Down
2 changes: 1 addition & 1 deletion packages/foundry/script/00_DeployMockTokens.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 run() external returns (address mockToken1, address mockToken2, address mockVeBAL) {
function deployMockTokens() internal returns (address mockToken1, address mockToken2, address mockVeBAL) {
// Start creating the transactions
uint256 deployerPrivateKey = getDeployerPrivateKey();
vm.startBroadcast(deployerPrivateKey);
Expand Down
30 changes: 15 additions & 15 deletions packages/foundry/script/01_DeployConstantSumPool.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { PoolHelpers, CustomPoolConfig, InitializationConfig } from "./PoolHelpers.sol";
import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol";
import { ConstantSumFactory } from "../contracts/factories/ConstantSumFactory.sol";
import { LotteryHook } from "../contracts/hooks/LotteryHook.sol";
import { VeBALFeeDiscountHook } from "../contracts/hooks/VeBALFeeDiscountHook.sol";

/**
* @title Deploy Constant Sum Pool
* @notice Deploys, registers, and initializes a constant sum pool that uses the Lottery Hook
* @notice Deploys, registers, and initializes a constant sum pool that uses a swap fee discount hook
*/
contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
function run(address token1, address token2) external {
function deployConstantSumPool(address token1, address token2, address veBAL) internal {
// Set the pool's deployment, registration, and initialization config
CustomPoolConfig memory poolConfig = getPoolConfig(token1, token2);
InitializationConfig memory initConfig = getInitializationConfig(token1, token2);
CustomPoolConfig memory poolConfig = getSumPoolConfig(token1, token2);
InitializationConfig memory initConfig = getSumPoolInitConfig(token1, token2);

// Start creating the transactions
uint256 deployerPrivateKey = getDeployerPrivateKey();
Expand All @@ -35,8 +35,10 @@ contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
console.log("Constant Sum Factory deployed at: %s", address(factory));

// Deploy a hook
address lotteryHook = address(new LotteryHook(vault, address(router)));
console.log("LotteryHook deployed at address: %s", lotteryHook);
address veBALFeeDiscountHook = address(
new VeBALFeeDiscountHook(vault, address(factory), address(router), veBAL)
);
console.log("VeBALFeeDiscountHook deployed at address: %s", veBALFeeDiscountHook);

// Deploy a pool and register it with the vault
address pool = factory.create(
Expand All @@ -47,15 +49,13 @@ contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
poolConfig.swapFeePercentage,
poolConfig.protocolFeeExempt,
poolConfig.roleAccounts,
lotteryHook, // poolHooksContract
veBALFeeDiscountHook, // poolHooksContract
poolConfig.liquidityManagement
);
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);
// Approve the router to spend tokens for pool initialization
approveRouterWithPermit2(initConfig.tokens);

// Seed the pool with initial liquidity
router.initialize(
Expand All @@ -76,7 +76,7 @@ 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 getPoolConfig(address token1, address token2) internal view returns (CustomPoolConfig memory config) {
function getSumPoolConfig(address token1, address token2) internal view returns (CustomPoolConfig 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
Expand Down Expand Up @@ -104,7 +104,7 @@ contract DeployConstantSumPool is PoolHelpers, ScaffoldHelpers {
poolCreator: address(0) // Account empowered to set the pool creator fee percentage
});
LiquidityManagement memory liquidityManagement = LiquidityManagement({
disableUnbalancedLiquidity: true, // Must be true to register with the Lottery Hook
disableUnbalancedLiquidity: false,
enableAddLiquidityCustom: false,
enableRemoveLiquidityCustom: false,
enableDonation: false
Expand All @@ -127,7 +127,7 @@ 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(
function getSumPoolInitConfig(
address token1,
address token2
) internal pure returns (InitializationConfig memory config) {
Expand Down
Loading

0 comments on commit ae8ca5e

Please sign in to comment.