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 - OrderHook #96

Open
wants to merge 2 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
476 changes: 476 additions & 0 deletions packages/foundry/contracts/hooks/OrderHook.sol

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions packages/foundry/contracts/hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# BAL Hookathon - OrderHoook

## Overview

OrderHook is a custom Balancer V3 hook designed to automate and enhance the management of token swaps in decentralized exchanges. This contract enables users to define conditional orders, such as stop-loss, take-profit, buy-stop and stop-limit orders, directly within a Balancer pool.

The hook is triggered after a swap occurs in the Balancer pool, capturing key data about the transaction (such as token addresses and their current prices). It emits this data as events, which can be processed by off-chain systems or other smart contracts to implement advanced order execution strategies or to notify users about the status of their orders.

## What the Hook Does

OrderHook is triggered after a swap operation in a Balancer pool. It is designed to:

- Emit Events for Post-Swap Processing: The hook captures essential information such as ```tokenIn```, ```tokenOut``` addresses, and their respective amounts swapped. These details are emitted as events, enabling off-chain systems or other smart contracts to react and process the swap results in real-time.

- Price Tracking and Analytics: The hook records the prices of ```tokenIn``` and ```tokenOut``` within the pool at the time of the swap. This data is emitted as part of the events, which can be used for analytics, on-chain reporting or to dynamically adjust strategies based on price movements.

- Support for Advanced Order Types: The hook processes four types of orders—Stop Loss, Buy Stop, Stop Limit and Take Profit. These advanced order types enable users to manage risk and automate trading strategies by executing swaps based on specific price thresholds or market conditions.

- On-Chain Strategy Execution: The hook’s event system can serve as a trigger for executing automated strategies, such as rebalancing portfolios, adjusting liquidity ratios or triggering conditional swaps based on predefined rules.

- Security and Auditing: By emitting detailed swap event data, the hook provides transparency and traceability for each transaction, which can be valuable for audits, risk management and security analysis.

## Example Use Case

Imagine a decentralized trading platform where users can place take-profit orders on token swaps. OrderHook can be integrated into this platform to facilitate order execution as follows:

- A user wants to make a swap that automatically executes when the price of a specific token reaches a certain threshold, allowing them to secure profits.
- The user submits their order to the OrderHook smart contract, specifying the token pair, desired price threshold, and swap parameters.
- When the token price in the Balancer pool meets or exceeds the specified threshold, OrderHook emits an event containing details such as the prices of tokenIn and tokenOut, their corresponding addresses, and the pool address.
- The platform's backend system or any backend service listens for these events and triggers a notification to the user, informing them that their take-profit conditions have been met.
- Based on this event, the trade is automatically executed to swap the tokens, or in case of slippage, the user can be refunded.

## Feedback About Developer Experience (DevX)

Working with the Balancer V3 hook framework has been an exciting experience. The modular design of the system makes it easy to plug in custom logic at various stages of the swap lifecycle. Here are a few insights from the development process:

- Flexibility: The hook architecture offers tremendous flexibility, allowing for custom workflows and integration with other DeFi systems like Uniswap or cross-chain operations.

- Documentation and Tooling: While the overall documentation for Balancer V3 was helpful, more examples and tooling specific to hooks would enhance the developer experience. Offering more templates or pre-built hooks could reduce development time and improve onboarding for new developers.
46 changes: 46 additions & 0 deletions packages/foundry/script/orderHook/00-DeployMockTokens.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {MockToken1} from "../src/mocks/MockToken1.sol";
import {MockToken2} from "../src/mocks/MockToken2.sol";
import {MockVeBAL} from "../src/mocks/MockVeBAL.sol";

import {HelperConfig} from "./HelperConfig.s.sol";

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";

/**
* @title Deploy Mock Tokens
* @notice Deploys mock tokens for use with pools and hooks
*/
contract DeployMockTokens is Script {
HelperConfig helperConfig;

function run() external {
deployMockTokens();
}

function deployMockTokens()
public
returns (address mockToken1, address mockToken2, address mockVeBAL)
{
helperConfig = new HelperConfig();
// Start creating the transactions
address deployer = helperConfig.getConfig().account;
console.log("Deployer: ", deployer);
vm.startBroadcast(deployer);

// Used to register & initialize pool contracts
mockToken1 = address(new MockToken1("Test Token 1", "AI", 1000e18));
mockToken2 = address(new MockToken2("Test Token 2", "AIS", 1000e18));
console.log("MockToken1 deployed at: %s", mockToken1);
console.log("MockToken2 deployed at: %s", mockToken2);

// Used for the VeBALFeeDiscountHook
mockVeBAL = address(new MockVeBAL("Vote-escrow BAL", "veBAL", 1000e18));
console.log("Mock Vote-escrow BAL deployed at: %s", mockVeBAL);

vm.stopBroadcast();
}
}
175 changes: 175 additions & 0 deletions packages/foundry/script/orderHook/01-DeployConstantSumPool.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {TokenConfig, TokenType, LiquidityManagement, PoolRoleAccounts} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.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/contracts/interfaces/contracts/vault/IVault.sol";
import {IRouter} from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol";

import {Router} from "@balancer-labs/contracts/vault/contracts/Router.sol";

import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";

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

import {ConstantSumFactory} from "../src/factories/ConstantSumFactory.sol";
import {OrderHook} from "../src/OrderHook.sol";

import {Script, console} from "forge-std/Script.sol";

import {HelperConfig} from "./HelperConfig.s.sol";
import {PoolHelpers, CustomPoolConfig, InitializationConfig} from "./PoolHelpers.sol";

/**
* @title Deploy Constant Sum Pool
* @notice Deploys, registers, and initializes a constant sum pool that uses a swap fee discount hook
*/
contract DeployConstantSumPool is Script, PoolHelpers {
function deployConstantSumPool(
address token1,
address token2
) public returns (address orderHook, address pool) {
HelperConfig helperConfig = new HelperConfig();
// Set the pool's deployment, registration, and initialization config
CustomPoolConfig memory poolConfig = getSumPoolConfig(token1, token2);
InitializationConfig memory initConfig = getSumPoolInitConfig(
token1,
token2
);

// Start creating the transactions
address deployer = helperConfig.getConfig().account;
vm.startBroadcast(deployer);

// Deploy a factory
ConstantSumFactory factory = new ConstantSumFactory(
IVault(helperConfig.getConfig().vault),
0 days
); // pauseWindowDuration
console.log("Constant Sum Factory deployed at: %s", address(factory));

// Deploy a hook
orderHook = address(
new OrderHook(
IVault(helperConfig.getConfig().vault),
address(factory),
helperConfig.getConfig().router,
IPermit2(helperConfig.getConfig().permit2)
)
);
console.log("OrderHook deployed at address: %s", orderHook);

// Deploy a pool and register it with the vault
pool = factory.create(
poolConfig.name,
poolConfig.symbol,
poolConfig.salt,
poolConfig.tokenConfigs,
poolConfig.swapFeePercentage,
poolConfig.protocolFeeExempt,
poolConfig.roleAccounts,
orderHook, // poolHooksContract
poolConfig.liquidityManagement
);
console.log("Constant Sum Pool deployed at: %s", pool);

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

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

/**
* @dev Set all of the configurations for deploying and registering a pool here
* @notice TokenConfig encapsulates the data required for the Vault to support a token of the given type.
* 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 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
uint256 swapFeePercentage = 0.01e18; // 1%
bool protocolFeeExempt = true;
address poolHooksContract = address(0); // zero address if no hooks contract is needed

TokenConfig[] memory tokenConfigs = new TokenConfig[](2); // An array of descriptors for the tokens the pool will manage.
tokenConfigs[0] = TokenConfig({ // Make sure to have proper token order (alphanumeric)
token: IERC20(token1),
tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE
rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above)
paysYieldFees: false // Flag indicating whether yield fees should be charged on this token
});
tokenConfigs[1] = TokenConfig({ // Make sure to have proper token order (alphanumeric)
token: IERC20(token2),
tokenType: TokenType.STANDARD, // STANDARD or WITH_RATE
rateProvider: IRateProvider(address(0)), // The rate provider for a token (see further documentation above)
paysYieldFees: false // Flag indicating whether yield fees should be charged on this token
});

PoolRoleAccounts memory roleAccounts = PoolRoleAccounts({
pauseManager: address(0), // Account empowered to pause/unpause the pool (or 0 to delegate to governance)
swapFeeManager: address(0), // Account empowered to set static swap fees for a pool (or 0 to delegate to goverance)
poolCreator: address(0) // Account empowered to set the pool creator fee percentage
});
LiquidityManagement memory liquidityManagement = LiquidityManagement({
disableUnbalancedLiquidity: false,
enableAddLiquidityCustom: false,
enableRemoveLiquidityCustom: false,
enableDonation: false
});

config = CustomPoolConfig({
name: name,
symbol: symbol,
salt: salt,
tokenConfigs: sortTokenConfig(tokenConfigs),
swapFeePercentage: swapFeePercentage,
protocolFeeExempt: protocolFeeExempt,
roleAccounts: roleAccounts,
poolHooksContract: poolHooksContract,
liquidityManagement: liquidityManagement
});
}

/**
* @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 getSumPoolInitConfig(
address token1,
address token2
) internal pure returns (InitializationConfig memory config) {
IERC20[] memory tokens = new IERC20[](2); // Array of tokens to be used in the pool
tokens[0] = IERC20(token1);
tokens[1] = IERC20(token2);
uint256[] memory exactAmountsIn = new uint256[](2); // Exact amounts of tokens to be added, sorted in token alphanumeric order
exactAmountsIn[0] = 50e18; // amount of token1 to send during pool initialization
exactAmountsIn[1] = 50e18; // amount of token2 to send during pool initialization
uint256 minBptAmountOut = 99e18; // Minimum amount of pool tokens to be received
bool wethIsEth = false; // If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens
bytes memory userData = bytes(""); // Additional (optional) data required for adding initial liquidity

config = InitializationConfig({
tokens: InputHelpers.sortTokens(tokens),
exactAmountsIn: exactAmountsIn,
minBptAmountOut: minBptAmountOut,
wethIsEth: wethIsEth,
userData: userData
});
}
}
Loading