Skip to content

Commit

Permalink
Configure default pools & hooks examples (#71)
Browse files Browse the repository at this point in the history
* refactor deploy scripts to be more modular

* Refactor types and comments for readability

* add hooks for each pool deployment script

* naive implementation for proportionally addding liquidity

* improve comment questions

* fix support for add liquidity proportional

* fix token balance update after remove liquidity

* fix pool actions layout shift

* add example tests for hooks
  • Loading branch information
MattPereira authored Aug 29, 2024
1 parent a5fbeee commit b3a938d
Show file tree
Hide file tree
Showing 32 changed files with 3,324 additions and 744 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,19 +215,26 @@ The deploy scripts are located in the [foundry/script/](https://github.com/balan
1. Deploys mock tokens that are used to register and initialize pools
2. Deploys a mock token used for the example `VeBALFeeDiscountHook` contract
#### [01_DeployConstantSum.s.sol](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/script/01_DeployConstantSum.s.sol)
#### [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_DeployConstantProduct.s.sol](https://github.com/balancer/scaffold-balancer-v3/blob/main/packages/foundry/script/02_DeployConstantProduct.s.sol)
#### [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 allows pools created by the `ConstantProductFactory`
3. Deploys and registers a `ConstantProductPool` that uses the `VeBALFeeDiscountHook`
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. Broadcast the Transactions 📡
To run all the deploy scripts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol";

import { ConstantProductPool } from "./ConstantProductPool.sol";
import { ConstantProductPool } from "../pools/ConstantProductPool.sol";

/**
* @title Constant Product Factory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { BasePoolFactory } from "@balancer-labs/v3-pool-utils/contracts/BasePoolFactory.sol";

import { ConstantSumPool } from "./ConstantSumPool.sol";
import { ConstantSumPool } from "../pools/ConstantSumPool.sol";

/**
* @title Constant Sum Factory
Expand Down
158 changes: 158 additions & 0 deletions packages/foundry/contracts/hooks/ExitFeeHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import {
AddLiquidityKind,
AddLiquidityParams,
LiquidityManagement,
RemoveLiquidityKind,
TokenConfig,
HookFlags
} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";

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

/**
* @notice Impose an "exit fee" on a pool. The value of the fee is returned to the LPs.
* @dev This hook extracts a fee on all withdrawals, then donates it back to the pool (effectively increasing the value
* of BPT shares for all users).
*
* Since the Vault always takes fees on the calculated amounts, and only supports taking fees in tokens, this hook
* must be restricted to pools that require proportional liquidity operations. The calculated amount for EXACT_OUT
* withdrawals would be in BPT, and charging fees on BPT is unsupported.
*
* Since the fee must be taken *after* the `amountOut` is calculated - and the actual `amountOut` returned to the Vault
* must be modified in order to charge the fee - `enableHookAdjustedAmounts` must also be set to true in the
* pool configuration. Otherwise, the Vault would ignore the adjusted values, and not recognize the fee.
*
* Finally, since the only way to deposit fee tokens back into the pool balance (without minting new BPT) is through
* the special "donation" add liquidity type, this hook also requires that the pool support donation.
*/
contract ExitFeeHook is BaseHooks, Ownable {
using FixedPoint for uint256;

// 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;

// Maximum exit fee of 10%
uint64 public constant MAX_EXIT_FEE_PERCENTAGE = 10e16;

/**
* @dev The exit fee cannot exceed the maximum allowed percentage.
* @param feePercentage The fee percentage exceeding the limit
* @param limit The maximum exit fee percentage
*/
error ExitFeeAboveLimit(uint256 feePercentage, uint256 limit);

/**
* @dev The pool does not support adding liquidity through donation.
* There is an existing similar error (IVaultErrors.DoesNotSupportDonation), but hooks should not throw
* "Vault" errors.
*/
error PoolDoesNotSupportDonation();

constructor(IVault vault) BaseHooks(vault) Ownable(msg.sender) {
// solhint-disable-previous-line no-empty-blocks
}

/// @inheritdoc IHooks
function onRegister(
address,
address,
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;
}

/// @inheritdoc IHooks
function getHookFlags() public pure override returns (HookFlags memory) {
HookFlags memory hookFlags;
// `enableHookAdjustedAmounts` must be true for all contracts that modify the `amountCalculated`
// in after hooks. Otherwise, the Vault will ignore any "hookAdjusted" amounts, and the transaction
// might not settle. (It should be false if the after hooks do something else.)
hookFlags.enableHookAdjustedAmounts = true;
hookFlags.shouldCallAfterRemoveLiquidity = true;
return hookFlags;
}

/// @inheritdoc IHooks
function onAfterRemoveLiquidity(
address,
address pool,
RemoveLiquidityKind kind,
uint256,
uint256[] memory,
uint256[] memory amountsOutRaw,
uint256[] memory,
bytes memory
) public override onlyVault returns (bool, uint256[] memory hookAdjustedAmountsOutRaw) {
// Our current architecture only supports fees on tokens. Since we must always respect exact `amountsOut`, and
// non-proportional remove liquidity operations would require taking fees in BPT, we only support proportional
// removeLiquidity.
if (kind != RemoveLiquidityKind.PROPORTIONAL) {
// Returning false will make the transaction revert, so the second argument does not matter.
return (false, amountsOutRaw);
}

IERC20[] memory tokens = _vault.getPoolTokens(pool);
uint256[] memory accruedFees = new uint256[](tokens.length);
hookAdjustedAmountsOutRaw = amountsOutRaw;

if (exitFeePercentage > 0) {
// Charge fees proportional to the `amountOut` of each token.
for (uint256 i = 0; i < amountsOutRaw.length; i++) {
uint256 exitFee = amountsOutRaw[i].mulDown(exitFeePercentage);
accruedFees[i] = exitFee;
hookAdjustedAmountsOutRaw[i] -= exitFee;
// Fees don't need to be transferred to the hook, because donation will redeposit them in the vault.
// In effect, we will transfer a reduced amount of tokensOut to the caller, and leave the remainder
// in the pool balance.
}

// Donates accrued fees back to LPs
_vault.addLiquidity(
AddLiquidityParams({
pool: pool,
to: msg.sender, // It would mint BPTs to router, but it's a donation so no BPT is minted
maxAmountsIn: accruedFees, // Donate all accrued fees back to the pool (i.e. to the LPs)
minBptAmountOut: 0, // Donation does not return BPTs, any number above 0 will revert
kind: AddLiquidityKind.DONATION,
userData: bytes("") // User data is not used by donation, so we can set it to an empty string
})
);
}

return (true, hookAdjustedAmountsOutRaw);
}

// Permissioned functions

/**
* @notice Sets the hook remove liquidity fee percentage, charged on every remove liquidity operation.
* @dev This function must be permissioned.
*/
function setExitFeePercentage(uint64 newExitFeePercentage) external onlyOwner {
if (newExitFeePercentage > MAX_EXIT_FEE_PERCENTAGE) {
revert ExitFeeAboveLimit(newExitFeePercentage, MAX_EXIT_FEE_PERCENTAGE);
}
exitFeePercentage = newExitFeePercentage;
}
}
Loading

0 comments on commit b3a938d

Please sign in to comment.