Skip to content
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
69 changes: 69 additions & 0 deletions packages/foundry/contracts/hooks/MevTaxHook.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 { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";
import { PoolSwapParams, TokenConfig, LiquidityManagement, HookFlags } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol";
import { VaultGuard } from "@balancer-labs/v3-vault/contracts/VaultGuard.sol";

contract MevTaxHook is BaseHooks, VaultGuard {
using FixedPoint for uint256;

address private immutable _factory;
uint256 public immutable MEV_TAX_MULTIPLIER;

constructor(IVault vault, address factory, uint256 mevTaxMultiplier) BaseHooks() VaultGuard(vault) {
_factory = factory;
MEV_TAX_MULTIPLIER = mevTaxMultiplier;
}

function onRegister(
address factory,
address pool,
TokenConfig[] memory,
LiquidityManagement calldata
) public view override onlyVault returns (bool) {
// Ensure:
// * factory matches the one provided in the constructor
// * pool is from the expected factory
return (factory == _factory && IBasePoolFactory(factory).isPoolFromFactory(pool));
}

function getHookFlags() public pure override returns (HookFlags memory) {
HookFlags memory hookFlags;
hookFlags.shouldCallComputeDynamicSwapFee = true;
return hookFlags;
}

function onComputeDynamicSwapFeePercentage(
PoolSwapParams calldata,
address,
uint256 staticSwapFeePercentage
) public view override returns (bool success, uint256 dynamicSwapFeePercentage) {

// Default to static swap fee if there are gas price shenanigans going on
// such as unexpected gas behavior from builders/sequencers or query functions
// using placeholder values for `tx.gasprice` and/or `block.basefee`
if (tx.gasprice < block.basefee) {
return (true, staticSwapFeePercentage);
}

// Unchecked because of the check above
uint256 priorityFee;
unchecked {
priorityFee = tx.gasprice - block.basefee;
}

// Calculate MEV tax based on priority fee
uint256 mevTaxPercentage = MEV_TAX_MULTIPLIER.mulUp(priorityFee);

uint256 feeMultiplier = FixedPoint.ONE + mevTaxPercentage;

// Calculate swap fee as fn of static fee and fee multiplier
uint256 swapFeeOut = staticSwapFeePercentage.mulUp(feeMultiplier);

return (true, swapFeeOut);
}
}
11 changes: 11 additions & 0 deletions packages/foundry/contracts/hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# What the hook does
The MEV Tax Hook allows Balancer pools to capture their own MEV by charging a dynamic fee based on the priority of transactions interacting with them.

It calculates the priority fee, then multiplies this by a configurable tax multiplier to determine the additional fee percentage. This dynamic fee is added to the application's base fee, enabling it to capture a portion of the MEV generated by high-priority transactions.

# Example use case:
When an arbitrage bot detects an opportunity between a Balancer pool and another DEX, it will use a a high-priority transaction to capitalize on the price discrepancy. By implementing the MEV Tax Hook, the bot's transaction would incur a higher fee proportional to its priority. This additional fee would be captured by the pool itself, rather than being entirely extracted by miners or other arbitrageurs, thus aligning incentives and redistributing MEV more equitably among ecosystem participants.

# Feedback about DevX
* Matt's Intro to Scaffold was extremely helpful -- a concise but complete guide to getting started.
* The code surrounding hooks itself is very well commented. I honestly read the comments there long before even opening the hook docs.
4 changes: 2 additions & 2 deletions packages/foundry/script/03_DeployWeightedPool8020.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ contract DeployWeightedPool8020 is PoolHelpers, ScaffoldHelpers {
"80/20 Weighted Pool", // string name
"80-20-WP", // string symbol
getTokenConfigs(token1, token2), // TokenConfig[] tokenConfigs
getNormailzedWeights(), // uint256[] normalizedWeights
getNormalizedWeights(), // uint256[] normalizedWeights
getRoleAccounts(), // PoolRoleAccounts roleAccounts
0.001e18, // uint256 swapFeePercentage (.01%)
exitFeeHook, // address poolHooksContract
Expand Down Expand Up @@ -94,7 +94,7 @@ contract DeployWeightedPool8020 is PoolHelpers, ScaffoldHelpers {
}

/// @dev Set the weights for each token in the pool
function getNormailzedWeights() internal pure returns (uint256[] memory normalizedWeights) {
function getNormalizedWeights() internal pure returns (uint256[] memory normalizedWeights) {
normalizedWeights = new uint256[](2);
normalizedWeights[0] = uint256(80e16);
normalizedWeights[1] = uint256(20e16);
Expand Down
139 changes: 139 additions & 0 deletions packages/foundry/script/04_DeployWeightedPool5050.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//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 { MevTaxHook } from "../contracts/hooks/MevTaxHook.sol";

/**
* @title Deploy Weighted Pool 50/50
* @notice Deploys, registers, and initializes a 50/50 weighted pool that uses an Mev Tax Hook
*/

contract DeployWeightedPool5050 is PoolHelpers, ScaffoldHelpers {

uint256 private constant MEV_TAX_MULTIPLIER = 50e18;

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

// Start creating the transactions
uint256 deployerPrivateKey = getDeployerPrivateKey();
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
address mevTaxHook = address(new MevTaxHook(vault, address(factory), MEV_TAX_MULTIPLIER));
console.log("MevTaxHook deployed at address: %s", mevTaxHook);

// Deploy a pool and register it with the vault
/// @notice passing args directly to avoid stack too deep error
address pool = factory.create(
"50/50 Weighted Pool", // string name
"50-50-WP", // string symbol
getTokenConfigs5050(token1, token2), // TokenConfig[] tokenConfigs
getNormalizedWeights5050(), // uint256[] normalizedWeights
getRoleAccounts5050(), // PoolRoleAccounts roleAccounts
0.1e18, // uint256 swapFeePercentage (1%)
mevTaxHook, // address poolHooksContract
false, //bool enableDonation
true, // bool disableUnbalancedLiquidity
keccak256(abi.encode(block.number)) // bytes32 salt
);
console.log("Weighted Pool deployed at: %s", pool);

// 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();
}

/**
* @dev Set the token configs for the pool
* @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 getTokenConfigs5050(address token1, address token2) internal pure returns (TokenConfig[] memory tokenConfigs) {
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
});
sortTokenConfig(tokenConfigs);
}

/// @dev Set the weights for each token in the pool
function getNormalizedWeights5050() private pure returns (uint256[] memory normalizedWeights) {
normalizedWeights = new uint256[](2);
normalizedWeights[0] = uint256(50e16);
normalizedWeights[1] = uint256(50e16);
}

/// @dev Set the role accounts for the pool
function getRoleAccounts5050() private pure returns (PoolRoleAccounts memory roleAccounts) {
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
});
}

/// @dev Set the initialization config for the pool (i.e. the amount of tokens to be added)
function getWeightedPoolInitConfig5050(
address token1,
address token2
) private 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 = 49e18; // 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
});
}
}
7 changes: 6 additions & 1 deletion packages/foundry/script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DeployMockTokens } from "./00_DeployMockTokens.s.sol";
import { DeployConstantSumPool } from "./01_DeployConstantSumPool.s.sol";
import { DeployConstantProductPool } from "./02_DeployConstantProductPool.s.sol";
import { DeployWeightedPool8020 } from "./03_DeployWeightedPool8020.s.sol";
import { DeployWeightedPool5050 } from "./04_DeployWeightedPool5050.s.sol";

/**
* @title Deploy Script
Expand All @@ -18,7 +19,8 @@ contract DeployScript is
DeployMockTokens,
DeployConstantSumPool,
DeployConstantProductPool,
DeployWeightedPool8020
DeployWeightedPool8020,
DeployWeightedPool5050
{
function run() external scaffoldExport {
// Deploy mock tokens to use for the pools and hooks
Expand All @@ -32,6 +34,9 @@ contract DeployScript is

// Deploy, register, and initialize a weighted pool with an exit fee hook
deployWeightedPool8020(mockToken1, mockToken2);

// Deploy, register, and initialize a weighted pool with a mev tax hook
deployWeightedPool5050(mockToken1, mockToken2);
}

modifier scaffoldExport() {
Expand Down
Loading