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 - Serene Hook #104

Open
wants to merge 13 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
Binary file added .github/assets/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
296 changes: 22 additions & 274 deletions README.md

Large diffs are not rendered by default.

382 changes: 382 additions & 0 deletions packages/foundry/contracts/hooks/SereneHook.sol

Large diffs are not rendered by default.

416 changes: 416 additions & 0 deletions packages/foundry/contracts/hooks/SereneVeBalDiscountHook.sol

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IGaugeRegistry {
function getPoolGauge(address pool) external view returns (address);
}
48 changes: 48 additions & 0 deletions packages/foundry/contracts/hooks/interfaces/IQuestBoard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IQuestBoard {
enum QuestVoteType {
NORMAL,
BLACKLIST,
WHITELIST
}
enum QuestCloseType {
NORMAL,
ROLLOVER,
DISTRIBUTE
}

function createFixedQuest(
address gauge,
address rewardToken,
bool startNextPeriod,
uint48 duration,
uint256 rewardPerVote,
uint256 totalRewardAmount,
uint256 feeAmount,
QuestVoteType voteType,
QuestCloseType closeType,
address[] calldata voterList
) external returns (uint256);

function createRangedQuest(
address gauge,
address rewardToken,
bool startNextPeriod,
uint48 duration,
uint256 minRewardPerVote,
uint256 maxRewardPerVote,
uint256 totalRewardAmount,
uint256 feeAmount,
QuestVoteType voteType,
QuestCloseType closeType,
address[] calldata voterList
) external returns (uint256);

function platformFeeRatio() external view returns (uint256);

function getAllPeriodsForQuestId(uint256 questID) external view returns (uint48[] memory);

function getCurrentPeriod() external view returns (uint256);
}
82 changes: 82 additions & 0 deletions packages/foundry/contracts/hooks/utils/QuestSettingsRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";
import "../interfaces/IQuestBoard.sol";

/// @title QuestSettingsRegistry
/// @notice A contract to store quest settings for each incentive token
/// @author 0xtekgrinder & Kogaroshi
contract QuestSettingsRegistry is Ownable {
/*//////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/

struct QuestSettings {
uint48 duration;
uint256 minRewardPerVote;
uint256 maxRewardPerVote;
IQuestBoard.QuestVoteType voteType;
IQuestBoard.QuestCloseType closeType;
address[] voterList;
}

/*//////////////////////////////////////////////////////////////
MUTABLE VARIABLES
//////////////////////////////////////////////////////////////*/

mapping(address => QuestSettings) public questSettings;

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/

constructor(address initialOwner) Ownable(initialOwner) {}

/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/

/**
* @notice Get the quest settings for a specific incentive token
* @param incentiveToken The incentive token address
* @return QuestSettings The quest settings for the incentive token
*/
function getQuestSettings(address incentiveToken) external view returns (QuestSettings memory) {
return questSettings[incentiveToken];
}

/*//////////////////////////////////////////////////////////////
OWNER FUNCTIONS
//////////////////////////////////////////////////////////////*/

/**
* @notice Set the quest settings for a specific incentive token
* @param incentiveToken The incentive token address
* @param duration The duration of the quest
* @param minRewardPerVote The minimum reward per vote
* @param maxRewardPerVote The maximum reward per vote
* @param voteType The vote type
* @param closeType The close type
* @param voterList The list of voters
* @custom:require Owner
*/
function setQuestSettings(
address incentiveToken,
uint48 duration,
uint256 minRewardPerVote,
uint256 maxRewardPerVote,
IQuestBoard.QuestVoteType voteType,
IQuestBoard.QuestCloseType closeType,
address[] calldata voterList
) external onlyOwner {
questSettings[incentiveToken] = QuestSettings(
duration,
minRewardPerVote,
maxRewardPerVote,
voteType,
closeType,
voterList
);
}
}
17 changes: 17 additions & 0 deletions packages/foundry/contracts/mocks/GaugeRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title GaugeRegistry
/// @notice A mock contract to register gauges for pools
/// @dev This contract is used for testing purposes only while waiting from feedback of balancer team regarding the balancer v3 gauge registry
contract GaugeRegistry {
mapping(address pool => address gauge) public gauges;

function register(address pool, address gauge) external {
gauges[pool] = gauge;
}

function getPoolGauge(address pool) external view returns (address) {
return gauges[pool];
}
}
69 changes: 69 additions & 0 deletions packages/foundry/contracts/mocks/MockQuestBoard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "../hooks/interfaces/IQuestBoard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract MockQuestBoard is IQuestBoard {
uint256 public currentPeriod;
uint256 public platformFeeRatio;
mapping(uint256 => uint48[]) periodsForQuestId;

constructor(uint256 _platformFeeRatio, uint256 _currentPeriod) {
currentPeriod = _currentPeriod;
platformFeeRatio = _platformFeeRatio;
}

function setPlatformFeeRatio(uint256 ratio) external {
platformFeeRatio = ratio;
}

function setPeriod(uint256 period) external {
currentPeriod = period;
}

function setPeriodsForQuestId(uint256 questID, uint48[] calldata periods) external {
periodsForQuestId[questID] = periods;
}

function createRangedQuest(
address, // gauge
address rewardToken,
bool, // startNextPeriod
uint48, // duration
uint256, // minRewardPerVote
uint256, // maxRewardPerVote
uint256 totalRewardAmount,
uint256 feeAmount,
QuestVoteType, // voteType
QuestCloseType, // closeType
address[] calldata // voterList
) external returns (uint256) {
IERC20(rewardToken).transferFrom(msg.sender, address(this), totalRewardAmount + feeAmount);
return 1;
}

function createFixedQuest(
address, // gauge
address rewardToken,
bool, // startNextPeriod
uint48, // duration
uint256, // rewardPerVote
uint256 totalRewardAmount,
uint256 feeAmount,
QuestVoteType, // voteType
QuestCloseType, // closeType
address[] calldata // voterList
) external returns (uint256) {
IERC20(rewardToken).transferFrom(msg.sender, address(this), totalRewardAmount + feeAmount);
return 1;
}

function getAllPeriodsForQuestId(uint256 questID) external view returns (uint48[] memory) {
return periodsForQuestId[questID];
}

function getCurrentPeriod() external view returns (uint256) {
return currentPeriod;
}
}
2 changes: 1 addition & 1 deletion packages/foundry/lib/balancer-v3-monorepo
1 change: 1 addition & 0 deletions packages/foundry/remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@balancer-labs/v3-pool-utils/=lib/balancer-v3-monorepo/pkg/pool-utils/
@balancer-labs/v3-interfaces/=lib/balancer-v3-monorepo/pkg/interfaces/
@balancer-labs/v3-pool-weighted/=lib/balancer-v3-monorepo/pkg/pool-weighted/
@balancer-labs/v3-pool-stable/=lib/balancer-v3-monorepo/pkg/pool-stable/
@balancer-labs/v3-vault/=lib/balancer-v3-monorepo/pkg/vault/
permit2/=lib/permit2/
forge-gas-snapshot/=node_modules/forge-gas-snapshot/src/
Expand Down
138 changes: 138 additions & 0 deletions packages/foundry/script/04_DeploySerenePool.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {
TokenConfig,
TokenType,
LiquidityManagement,
PoolRoleAccounts
} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { IRateProvider } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/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 { PoolHelpers, InitializationConfig } from "./PoolHelpers.sol";
import { ScaffoldHelpers, console } from "./ScaffoldHelpers.sol";
import { WeightedPoolFactory } from "@balancer-labs/v3-pool-weighted/contracts/WeightedPoolFactory.sol";
import { ExitFeeHookExample } from "../contracts/hooks/ExitFeeHookExample.sol";

import { GaugeRegistry } from "../contracts/mocks/GaugeRegistry.sol";
import { MockQuestBoard } from "../contracts/mocks/MockQuestBoard.sol";
import { QuestSettingsRegistry } from "../contracts/hooks/utils/QuestSettingsRegistry.sol";
import { SereneHook } from "../contracts/hooks/SereneHook.sol";
import { IQuestBoard } from "../contracts/hooks/interfaces/IQuestBoard.sol";

import { DeployWeightedPool8020 } from "./03_DeployWeightedPool8020.s.sol";
import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol";

/**
* @title Deploy Weighted Pool 80/20
* @notice Deploys, registers, and initializes a 80/20 weighted pool that uses an Exit Fee Hook
*/
contract DeploySerenePool is DeployWeightedPool8020 {
struct SereneHookConstructorParams {
IVault vault;
IPermit2 permit2;
address factory;
address gaugeRegistry;
address batchRouter;
address questBoard;
address questSettings;
address token1;
uint64 fee;
}

function deploySerenePool(address token1, address token2) internal {
// Set the pool initialization config
InitializationConfig memory initConfig = getWeightedPoolInitConfig(token1, token2);

// Start creating the transactions
uint256 deployerPrivateKey = getDeployerPrivateKey();
address deployer = vm.addr(deployerPrivateKey);
vm.startBroadcast(deployerPrivateKey);

// Deploy a factory
WeightedPoolFactory factory = new WeightedPoolFactory(vault, 365 days, "Factory v1", "Pool v1");
console.log("Weighted Pool Factory deployed at: %s", address(factory));

// Deploy a hook
GaugeRegistry gaugeRegistry = new GaugeRegistry();
QuestSettingsRegistry questSettings = new QuestSettingsRegistry(deployer);
MockQuestBoard questBoard = new MockQuestBoard(400, 0);
questSettings.setQuestSettings(
address(token1),
1,
1000,
2000,
IQuestBoard.QuestVoteType.NORMAL,
IQuestBoard.QuestCloseType.NORMAL,
new address[](0)
);

address sereneHook = _deploySereneHook(
SereneHookConstructorParams(
vault,
permit2,
address(factory),
address(gaugeRegistry),
address(batchRouter),
address(questBoard),
address(questSettings),
address(token1),
5e17 // 50% of fee
)
);
console.log("SereneHook deployed at address: %s", sereneHook);

// Deploy a pool and register it with the vault
/// @notice passing args directly to avoid stack too deep error
address pool = factory.create(
"80/20 Weighted Pool", // string name
"80-20-WP", // string symbol
getTokenConfigs(token1, token2), // TokenConfig[] tokenConfigs
getNormailzedWeights(), // uint256[] normalizedWeights
getRoleAccounts(), // PoolRoleAccounts roleAccounts
0.001e18, // uint256 swapFeePercentage (.01%)
sereneHook, // address poolHooksContract
true, //bool enableDonation
true, // bool disableUnbalancedLiquidity (must be true for the ExitFee Hook)
keccak256(abi.encode(block.number)) // bytes32 salt
);
console.log("Weighted Pool deployed at: %s", pool);

// Add a fake gauge to the Gauge Registry
gaugeRegistry.register(address(pool), makeAddr("gauge"));

// Approve the router to spend tokens for pool initialization
approveRouterWithPermit2(initConfig.tokens);

// Seed the pool with initial liquidity using Router as entrypoint
router.initialize(
pool,
initConfig.tokens,
initConfig.exactAmountsIn,
initConfig.minBptAmountOut,
initConfig.wethIsEth,
initConfig.userData
);
console.log("Weighted Pool initialized successfully!");
vm.stopBroadcast();
}

function _deploySereneHook(SereneHookConstructorParams memory params) internal returns (address) {
return address(
new SereneHook(
params.vault,
params.permit2,
params.factory,
params.gaugeRegistry,
params.batchRouter,
params.questBoard,
params.questSettings,
params.token1,
params.fee
)
);
}
}
Loading