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

BAL Hookathon - DynamicFeeHook #101

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
320 changes: 42 additions & 278 deletions README.md

Large diffs are not rendered by default.

Binary file added img/feecalculation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/hookTest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/liquidity.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
398 changes: 398 additions & 0 deletions packages/foundry/contracts/hooks/DynamicFeeHook.sol

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions packages/foundry/contracts/interfaces/IDynamicFeeHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

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

interface IDynamicFeeHook {
struct LockInfo {
uint128 bptLocked;
uint128 accruedRewards;
uint128 rewardDebt;
uint128 lockStart;
}

struct PoolInfo {
uint128 bptLocked;
uint128 lastRewardBalance;
uint128 accRewardsPerShare;
address feeToken;
address rewardToken; // an aToken of the AAVE lending
}

// Events

event DynamicFeeHookRegistered(address indexed hooksContract, address indexed factory, address indexed pool);
event HookFeeInvested(address indexed hooksContract, IERC20 indexed token, uint256 feeAmount);
event InvestPoolAdded(address _pool, address _asset);
event EarlyUnlockSet(bool _allowedEarlyUnlock);
event MinLockDurationSet(uint256 _minLockDuration);

// Errors

error AlreadyExist();
}
49 changes: 49 additions & 0 deletions packages/foundry/contracts/interfaces/ILendingPoolV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

interface ILendingPoolV3 {
struct ReserveConfigurationMap {
uint256 data;
}

struct ReserveDataLegacy {
//stores the reserve configuration
ReserveConfigurationMap configuration;
//the liquidity index. Expressed in ray
uint128 liquidityIndex;
//the current supply rate. Expressed in ray
uint128 currentLiquidityRate;
//variable borrow index. Expressed in ray
uint128 variableBorrowIndex;
//the current variable borrow rate. Expressed in ray
uint128 currentVariableBorrowRate;
//the current stable borrow rate. Expressed in ray
uint128 currentStableBorrowRate;
//timestamp of last update
uint40 lastUpdateTimestamp;
//the id of the reserve. Represents the position in the list of the active reserves
uint16 id;
//aToken address
address aTokenAddress;
//stableDebtToken address
address stableDebtTokenAddress;
//variableDebtToken address
address variableDebtTokenAddress;
//address of the interest rate strategy
address interestRateStrategyAddress;
//the current treasury balance, scaled
uint128 accruedToTreasury;
//the outstanding unbacked aTokens minted through the bridging feature
uint128 unbacked;
//the outstanding debt borrowed against this asset in isolation mode
uint128 isolationModeTotalDebt;
}

function getReserveData(address asset) external view returns (ReserveDataLegacy memory);

// @notice Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens.
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;

// @notice Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned
function withdraw(address asset, uint256 amount, address to) external returns (uint256);
}
10 changes: 10 additions & 0 deletions packages/foundry/contracts/interfaces/IRouterCommon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

interface IRouterCommon {
/**
* @notice Get the first sender which initialized the call to Router.
* @return address The sender address
*/
function getSender() external view returns (address);
}
166 changes: 166 additions & 0 deletions packages/foundry/test/DynamicFeeHook.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import "forge-std/Test.sol";

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.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,
SwapKind,
TokenConfig,
PoolSwapParams
} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";

import { CastingHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/CastingHelpers.sol";
import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol";
import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";
import { StableMath } from "@balancer-labs/v3-solidity-utils/contracts/math/StableMath.sol";

import { StablePoolFactory } from "../lib/balancer-v3-monorepo/pkg/pool-stable/contracts/StablePoolFactory.sol";
import { PoolMock } from "@balancer-labs/v3-vault/contracts/test/PoolMock.sol";

import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol";

import { ATokenMock } from "./mock/ATokenMock.sol";
import { FeeTokenMock } from "./mock/FeeTokenMock.sol";
import { LendingPoolMock } from "./mock/LendingPoolMock.sol";
import { DynamicFeeHook } from "../contracts/hooks/DynamicFeeHook.sol";
import { IDynamicFeeHook } from "../contracts/interfaces/IDynamicFeeHook.sol";
import { ILendingPoolV3 } from "../contracts/interfaces/ILendingPoolV3.sol";

contract DynamicFeeHookTest is BaseVaultTest {
using CastingHelpers for address[];
using FixedPoint for uint256;
using ArrayHelpers for *;

StablePoolFactory internal stablePoolFactory;

address internal dynamicFeeHook;
IERC20 internal feeToken;
IERC20 internal rewardToken;
ILendingPoolV3 internal lendingPool;

uint256 internal constant DEFAULT_AMP_FACTOR = 200;

uint256 internal constant SWAP_FEE_PERCENTAGE = 5e15; // 0.5%

uint256 internal constant MIN_FEE = 10e15; // 0.1%
uint256 internal constant MAX_FEE = 10e16; // 1.0%
uint256 internal constant VOLATILITY_SENSITIVITY = 10e14; // 0.01%
uint256 internal constant LIQUIDITY_SENSITIVITY = 5e13; // 0.005%
uint256 internal constant MIN_LOCK_DURATION = 4 weeks;

function setUp() public override {
super.setUp();

// Deploy fee, reward tokens and lending pool
feeToken = new FeeTokenMock();
rewardToken = new ATokenMock();
lendingPool = new LendingPoolMock(address(feeToken), address(rewardToken));
}

function createHook() internal override returns (address) {
// Create the factory here, because it needs to be deployed after the Vault, but before the hook contract.
stablePoolFactory = new StablePoolFactory(IVault(address(vault)), 365 days, "Factory v1", "Pool v1");
// lp will be the owner of the hook.
vm.prank(admin);
dynamicFeeHook = address(
new DynamicFeeHook(
IVault(address(vault)),
address(stablePoolFactory),
MIN_FEE,
MAX_FEE,
VOLATILITY_SENSITIVITY,
LIQUIDITY_SENSITIVITY,
MIN_LOCK_DURATION,
address(lendingPool)
)
);
vm.label(dynamicFeeHook, "Dynamic Fee Hook");
return dynamicFeeHook;
}

function _createPool(address[] memory tokens, string memory label) internal override returns (address) {
PoolRoleAccounts memory roleAccounts;

vm.expectEmit(true, true, false, false);
emit IDynamicFeeHook.DynamicFeeHookRegistered(dynamicFeeHook, address(stablePoolFactory), address(0));

address newPool = address(
stablePoolFactory.create(
"Stable Pool Test",
"STABLE-TEST",
vault.buildTokenConfig(tokens.asIERC20()),
DEFAULT_AMP_FACTOR,
roleAccounts,
BASE_MIN_SWAP_FEE,
poolHooksContract,
false, // Does not allow donations
false, // Do not disable unbalanced add/remove liquidity
ZERO_BYTES32
)
);
vm.label(newPool, label);

authorizer.grantRole(vault.getActionId(IVaultAdmin.setStaticSwapFeePercentage.selector), admin);
vm.prank(admin);
vault.setStaticSwapFeePercentage(newPool, SWAP_FEE_PERCENTAGE);

return newPool;
}

function testRegistryWithWrongFactory() public {
address dynamicFeePool = _createPoolToRegister();
TokenConfig[] memory tokenConfig = vault.buildTokenConfig(
[address(dai), address(usdc)].toMemoryArray().asIERC20()
);

// Registration fails because this factory is not allowed to register the hook.
vm.expectRevert(
abi.encodeWithSelector(
IVaultErrors.HookRegistrationFailed.selector,
poolHooksContract,
dynamicFeePool,
address(factoryMock)
)
);
_registerPoolWithHook(dynamicFeePool, tokenConfig);
}

function testSuccessfulRegistry() public view {
HooksConfig memory hooksConfig = vault.getHooksConfig(pool);

assertEq(hooksConfig.hooksContract, poolHooksContract, "hooksContract is wrong");
assertTrue(hooksConfig.shouldCallComputeDynamicSwapFee, "shouldCallComputeDynamicSwapFee is false");
assertTrue(hooksConfig.shouldCallAfterAddLiquidity, "shouldCallAfterAddLiquidity is false");
assertTrue(hooksConfig.shouldCallAfterRemoveLiquidity, "shouldCallAfterRemoveLiquidity is false");
assertTrue(hooksConfig.shouldCallAfterSwap, "shouldCallAfterSwap is false");
assertTrue(hooksConfig.shouldCallBeforeSwap, "shouldCallBeforeSwap is false");
}

function testInvestingFlow() public view {}

// Registration tests require a new pool, because an existing pool may already be registered.
function _createPoolToRegister() private returns (address newPool) {
//newPool = address(deployPoolMock(IVault(address(vault)), "ERC20 Pool", "ERC20POOL"));
// newPool = new PoolMock(IVault(address(vault)), "ERC20 Pool", "ERC20POOL").address();
newPool = address(new PoolMock(IVault(address(vault)), "ERC20 Pool", "ERC20POOL"));
vm.label(newPool, "Directional Fee Pool");
}

function _registerPoolWithHook(address dynamicFeePool, TokenConfig[] memory tokenConfig) private {
PoolRoleAccounts memory roleAccounts;
roleAccounts.poolCreator = lp;

LiquidityManagement memory liquidityManagement;

factoryMock.registerPool(dynamicFeePool, tokenConfig, roleAccounts, poolHooksContract, liquidityManagement);
}
}
27 changes: 27 additions & 0 deletions packages/foundry/test/mock/ATokenMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

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

contract ATokenMock is ERC20("A-token", "A-token") {
uint256 public startBlock = block.number;

// increases over block
function balanceOf(address account) public view override returns (uint256) {
uint256 balance = super.balanceOf(account);
uint256 profit = balance * (block.timestamp - startBlock) / startBlock;
return balance + profit;
}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}

function burn(address from, uint256 amount) public {
if (amount > balanceOf(from)) {
amount = balanceOf(from);
}

_burn(from, amount);
}
}
17 changes: 17 additions & 0 deletions packages/foundry/test/mock/FeeTokenMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

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

contract FeeTokenMock is ERC20("FeeToken", "FT") {
function mint(address to, uint256 amount) external {
_mint(to, amount);
}

function burn(address from, uint256 amount) external {
if (amount > balanceOf(from)) {
amount = balanceOf(from);
}
_burn(from, amount);
}
}
55 changes: 55 additions & 0 deletions packages/foundry/test/mock/LendingPoolMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ATokenMock } from "./ATokenMock.sol";
import { FeeTokenMock } from "./FeeTokenMock.sol";
import { ILendingPoolV3 } from "../../contracts/interfaces/ILendingPoolV3.sol";

contract LendingPoolMock is ILendingPoolV3 {
address public feeToken;
address public aToken;

constructor(address _feeToken, address _aToken) {
feeToken = _feeToken;
aToken = _aToken;
}

function supply(address asset, uint256 amount, address onBehalfOf, uint16 /*referralCode*/) external {
FeeTokenMock(asset).transferFrom(msg.sender, address(this), amount);

ATokenMock(asset).mint(onBehalfOf, amount);
return;
}

function withdraw(address asset, uint256 amount, address to) external returns (uint256) {
ATokenMock(asset).burn(to, amount);

FeeTokenMock(asset).mint(msg.sender, amount);
return amount;
}

function getReserveData(address) external view returns (ReserveDataLegacy memory) {
ILendingPoolV3.ReserveConfigurationMap memory config = ReserveConfigurationMap(0);
config.data = 0;

return
ReserveDataLegacy({
configuration: config,
liquidityIndex: 0,
currentLiquidityRate: 0,
variableBorrowIndex: 0,
currentVariableBorrowRate: 0,
currentStableBorrowRate: 0,
lastUpdateTimestamp: 0,
id: 0,
aTokenAddress: aToken,
stableDebtTokenAddress: address(0),
variableDebtTokenAddress: address(0),
interestRateStrategyAddress: address(0),
accruedToTreasury: 0,
unbacked: 0,
isolationModeTotalDebt: 0
});
}
}