-
Notifications
You must be signed in to change notification settings - Fork 45
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
hook submission #103
base: main
Are you sure you want to change the base?
hook submission #103
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import "Balancer/BaseHooks.sol"; | ||
import "Balancer/VaultGuard.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | ||
|
||
contract CommitMinerHook is BaseHooks, VaultGuard, ReentrancyGuard { | ||
struct Commit { | ||
bytes32 hash; // The commit hash | ||
uint256 blockNumber; // The block number when the commit was created | ||
address pool; // The pool that generated the commit | ||
address swapper; // Address of the user who performed the swap | ||
} | ||
|
||
IERC20 public immutable paymentToken; // The ERC20 token used for both payments and rewards | ||
address private immutable hookDeployer; // Address of the hook deployer | ||
Commit[] public commitBacklog; // Array storing all commits | ||
uint256 public feePercentage = 10; // 10% fee to hook deployer, 30% swappers, 60% pool LPs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be more consistent with the rest of the system to use FixedPoint math (e.g., 10% = 10e16). |
||
|
||
event CommitGenerated(address indexed pool, address indexed swapper, bytes32 commitHash, uint256 blockNumber); | ||
event RandomnessRequested(address indexed requester, uint256 amountPaid, uint256 commitCount); | ||
event FeeDistributed(address indexed pool, address indexed recipient, uint256 amount); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be useful to have events for each fee type, for ease of tracking. But looking at the code below, I don't think you can track at this level of detail unless the # of commits were very low. Probably just a single event for the total fee, and an event on deployment that shows the split. |
||
|
||
constructor(IVault vault, IERC20 _paymentToken) VaultGuard(vault) { | ||
hookDeployer = msg.sender; | ||
paymentToken = _paymentToken; // The specified ERC20 token for all payments and rewards | ||
} | ||
|
||
function getHookFlags() public pure override returns (IHooks.HookFlags memory hookFlags) { | ||
hookFlags.shouldCallBeforeSwap = true; | ||
} | ||
|
||
/// @inheritdoc IHooks | ||
function onRegister(address factory, address pool, ...) public override onlyVault returns (bool) { | ||
return true; | ||
} | ||
|
||
// Hook triggered before a swap to create a commit | ||
function onBeforeSwap( | ||
address pool, | ||
uint256 amountIn, | ||
uint256 amountOut, | ||
address tokenIn, | ||
address tokenOut, | ||
address user | ||
) external override onlyVault { | ||
bytes32 commitHash = keccak256(abi.encodePacked(block.timestamp, user, amountIn, amountOut, pool, gasleft())); | ||
commitBacklog.push(Commit(commitHash, block.number, pool, user)); | ||
|
||
emit CommitGenerated(pool, user, commitHash, block.number); | ||
} | ||
|
||
// Randomness requester pays for a batch of commits using the specified paymentToken | ||
function requestRandomness(uint256 commitCount, uint256 amountPaid) external nonReentrant { | ||
require(commitCount <= commitBacklog.length, "Not enough commits available"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This grows without bound, so there would have to be some kind of validation here in production. Like using a circular buffer instead of a simple array (1024 or 2048), and limiting the |
||
|
||
// Transfer payment from requester | ||
require(paymentToken.transferFrom(msg.sender, address(this), amountPaid), "Payment transfer failed"); | ||
|
||
// Generate randomness based on the selected number of commits | ||
bytes32 finalRandomness = aggregateCommits(commitCount); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does this value go? Shouldn't it be returned or something? |
||
|
||
// Emit event for randomness request | ||
emit RandomnessRequested(msg.sender, amountPaid, commitCount); | ||
|
||
// Distribute fees | ||
distributeFees(amountPaid, commitCount); | ||
} | ||
|
||
// Function to aggregate commits into randomness | ||
function aggregateCommits(uint256 commitCount) internal view returns (bytes32) { | ||
bytes32 randomness; | ||
for (uint256 i = commitBacklog.length - commitCount; i < commitBacklog.length; i++) { | ||
randomness = keccak256(abi.encodePacked(randomness, commitBacklog[i].hash, block.number, gasleft())); | ||
} | ||
return randomness; | ||
} | ||
|
||
// Function to distribute fees among swappers, LPs, and the hook deployer | ||
function distributeFees(uint256 amountPaid, uint256 commitCount) internal nonReentrant { | ||
uint256 hookFee = (amountPaid * 10) / 100; // 10% to hook deployer | ||
uint256 swapperFee = (amountPaid * 30) / 100; // 30% to swappers | ||
uint256 lpFee = amountPaid - hookFee - swapperFee; // 60% to pool LPs | ||
|
||
// Transfer 10% to hook deployer | ||
require(paymentToken.transfer(hookDeployer, hookFee), "Hook deployer fee transfer failed"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would use |
||
|
||
// Distribute 30% to the swappers | ||
uint256 perSwapperFee = swapperFee / commitCount; | ||
for (uint256 i = commitBacklog.length - commitCount; i < commitBacklog.length; i++) { | ||
require(paymentToken.transfer(commitBacklog[i].swapper, perSwapperFee), "Swapper fee transfer failed"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a lot of transfers! If you're going to try to do this, consider a different data structure that aggregates swappers, since active users might have 20-30 swaps, and doing 30 transfers is prohibitive (not to mention all the event emission). |
||
emit FeeDistributed(commitBacklog[i].pool, commitBacklog[i].swapper, perSwapperFee); | ||
} | ||
|
||
// Distribute 60% to the LPs | ||
uint256 perPoolFee = lpFee / commitCount; | ||
for (uint256 i = commitBacklog.length - commitCount; i < commitBacklog.length; i++) { | ||
require(paymentToken.transfer(commitBacklog[i].pool, perPoolFee), "LP fee transfer failed"); // Simplified; could route to LPs via pool logic | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tokens would be locked/lost. You can't just transfer tokens to the pool address (since v1, anyway). You'd have to use the DONATION add liquidity method - and check on registration that the pool supports it. same comment about aggregation transfers. |
||
emit FeeDistributed(commitBacklog[i].pool, msg.sender, perPoolFee); | ||
} | ||
} | ||
|
||
// Use quadratic pricing model for commit pricing | ||
function calculateFeeForCommits(uint256 commitCount) internal view returns (uint256) { | ||
uint256 basePrice = 1e18; // 1 token as base price per commit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assumes 18 decimals. Would either need to constrain it to 18-decimal tokens only, or make some provision for scaling. |
||
return commitCount ** 2 * basePrice; // Quadratic pricing | ||
} | ||
|
||
// Allow the user to pay more for more commits to improve randomness quality | ||
function payMoreForMoreCommits(uint256 commitWindowSize, uint256 additionalAmount) external nonReentrant { | ||
uint256 amountRequired = calculateFeeForCommits(commitWindowSize); | ||
require(additionalAmount >= amountRequired, "Insufficient payment"); | ||
|
||
// Proceed with larger batch of commits | ||
requestRandomness(commitWindowSize, additionalAmount); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import "forge-std/Test.sol"; | ||
import "../src/CommitMinerHook.sol"; | ||
import "openzeppelin-contracts/token/ERC20/IERC20.sol"; | ||
|
||
contract CommitMinerHookTest is Test { | ||
CommitMinerHook public commitMinerHook; | ||
IERC20 public paymentToken; | ||
address public pool; | ||
address public swapper; | ||
address public lps; | ||
address public deployer; | ||
|
||
function setUp() public { | ||
// Mock addresses | ||
pool = address(0x123); | ||
swapper = address(0x456); | ||
lps = address(0x789); | ||
deployer = address(this); | ||
|
||
// Deploy a mock ERC20 payment token | ||
paymentToken = IERC20(address(new MockERC20())); | ||
|
||
// Deploy the CommitMinerHook contract | ||
IVault vault = IVault(address(0x111)); | ||
commitMinerHook = new CommitMinerHook(vault, paymentToken); | ||
} | ||
|
||
// Test commit generation on a swap | ||
function testCommitGeneratedOnSwap() public { | ||
// Set up mock swap details | ||
uint256 amountIn = 1000 * 1e18; | ||
uint256 amountOut = 900 * 1e18; | ||
address tokenIn = address(0x222); | ||
address tokenOut = address(0x333); | ||
|
||
// Simulate a swap and trigger the onBeforeSwap hook | ||
vm.prank(swapper); | ||
commitMinerHook.onBeforeSwap(pool, amountIn, amountOut, tokenIn, tokenOut, swapper); | ||
|
||
// Verify that a commit has been generated | ||
(bytes32 hash, uint256 blockNumber, address poolAddress, address swapperAddress) = commitMinerHook.commitBacklog(0); | ||
assertEq(poolAddress, pool); | ||
assertEq(swapperAddress, swapper); | ||
} | ||
|
||
// Test randomness request and fee transfer | ||
function testRandomnessRequestAndFeeTransfer() public { | ||
// Set up mock commits | ||
createMockCommits(3); | ||
|
||
// Set up fee for requesting randomness | ||
uint256 commitCount = 2; | ||
uint256 fee = commitMinerHook.calculateFeeForCommits(commitCount); | ||
|
||
// Mint tokens to the randomness requester | ||
paymentToken.mint(address(this), fee); | ||
|
||
// Approve the contract to spend the tokens | ||
paymentToken.approve(address(commitMinerHook), fee); | ||
|
||
// Request randomness | ||
commitMinerHook.requestRandomness(commitCount, fee); | ||
|
||
// Verify the fee has been transferred and distributed | ||
assertEq(paymentToken.balanceOf(deployer), fee / 10); // 10% to deployer | ||
// Further checks for swapper and LP distribution can be done here | ||
} | ||
|
||
// Test quadratic pricing model for commits | ||
function testQuadraticPricingForCommits() public { | ||
uint256 commitCount = 4; | ||
uint256 expectedFee = 16 * 1e18; // 4^2 * 1e18 | ||
uint256 calculatedFee = commitMinerHook.calculateFeeForCommits(commitCount); | ||
|
||
// Verify the calculated fee matches the quadratic pricing model | ||
assertEq(calculatedFee, expectedFee); | ||
} | ||
|
||
// Test fee distribution to swappers, LPs, and deployer | ||
function testFeeDistribution() public { | ||
// Set up mock commits and fees | ||
createMockCommits(3); | ||
uint256 commitCount = 2; | ||
uint256 fee = commitMinerHook.calculateFeeForCommits(commitCount); | ||
paymentToken.mint(address(this), fee); | ||
paymentToken.approve(address(commitMinerHook), fee); | ||
|
||
// Request randomness to trigger fee distribution | ||
commitMinerHook.requestRandomness(commitCount, fee); | ||
|
||
// Verify fee distribution to hook deployer, swappers, and LPs | ||
uint256 hookDeployerFee = (fee * 10) / 100; | ||
uint256 swapperFee = (fee * 30) / 100; | ||
uint256 lpFee = fee - hookDeployerFee - swapperFee; | ||
|
||
assertEq(paymentToken.balanceOf(deployer), hookDeployerFee); | ||
assertEq(paymentToken.balanceOf(swapper), swapperFee / commitCount); // 30% split between swappers | ||
assertEq(paymentToken.balanceOf(pool), lpFee / commitCount); // 60% to LPs | ||
} | ||
|
||
// Helper function to create mock commits | ||
function createMockCommits(uint256 numCommits) internal { | ||
for (uint256 i = 0; i < numCommits; i++) { | ||
vm.prank(swapper); | ||
commitMinerHook.onBeforeSwap(pool, 1000 * 1e18, 900 * 1e18, address(0x111), address(0x222), swapper); | ||
} | ||
} | ||
} | ||
|
||
// Mock ERC20 contract to simulate payment token behavior | ||
contract MockERC20 is IERC20 { | ||
string public name = "Mock ERC20"; | ||
string public symbol = "MERC20"; | ||
uint8 public decimals = 18; | ||
uint256 public totalSupply; | ||
mapping(address => uint256) public balanceOf; | ||
mapping(address => mapping(address => uint256)) public allowance; | ||
|
||
function transfer(address recipient, uint256 amount) public override returns (bool) { | ||
balanceOf[msg.sender] -= amount; | ||
balanceOf[recipient] += amount; | ||
return true; | ||
} | ||
|
||
function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { | ||
allowance[sender][msg.sender] -= amount; | ||
balanceOf[sender] -= amount; | ||
balanceOf[recipient] += amount; | ||
return true; | ||
} | ||
|
||
function approve(address spender, uint256 amount) public override returns (bool) { | ||
allowance[msg.sender][spender] = amount; | ||
return true; | ||
} | ||
|
||
function mint(address to, uint256 amount) public { | ||
totalSupply += amount; | ||
balanceOf[to] += amount; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
Commit Miner Hook | ||
Overview | ||
The Commit Miner Hook is a custom hook designed for use within the Balancer V3 ecosystem. It aggregates commits (hashes) from swaps in different liquidity pools and allows users to request randomness based on these commits. This randomness generation is decentralized, and randomness requesters pay a fee for it. The fees collected are distributed between swappers, liquidity providers (LPs), and the hook deployer. | ||
|
||
This system leverages the commit and reveal scheme to aggregate transaction-based data, providing randomness for use cases like gaming, lotteries, or decentralized oracle systems while generating additional yield for liquidity providers and participants. | ||
|
||
What the Hook Does | ||
Commit Generation: | ||
|
||
Every time a swap occurs in a pool that uses the Commit Miner Hook, the hook generates a commit hash based on swap details (e.g., token amounts, user, block data) and stores it in the contract's commit backlog. | ||
Randomness Request: | ||
|
||
Users can request randomness by paying a fee in a specified ERC20 payment token. The fee scales quadratically based on the number of commits the user wants to use to generate randomness. | ||
The contract aggregates a set number of commits from the backlog and generates a random value for the requester. | ||
Fee Distribution: | ||
|
||
The fee collected from randomness requests is distributed to three parties: | ||
10% to the hook deployer. | ||
30% to the swappers who generated the commits used in the randomness generation. | ||
60% to the liquidity providers (LPs) of the pools from which the commits originated. | ||
Quadratic Pricing Model: | ||
|
||
The pricing for requesting randomness increases quadratically based on the number of commits used in the randomness generation (i.e., larger requests are progressively more expensive). | ||
Example Use Case | ||
Imagine you are running a decentralized lottery and need a source of randomness for drawing winners. You could use the Commit Miner Hook to generate randomness based on real-time swap data from various Balancer pools. Here’s how it could work: | ||
|
||
Setup: | ||
|
||
The hook is integrated into Balancer pools, and the system starts aggregating commits from swaps happening in these pools. | ||
Randomness Request: | ||
|
||
When the lottery system needs randomness to draw a winner, it calls the requestRandomness function on the Commit Miner Hook contract and pays a fee in the specified ERC20 token. | ||
The contract bundles recent commits and generates a random number, which can then be used to draw a lottery winner. | ||
Fee Distribution: | ||
|
||
The fee paid by the lottery contract is split between the swappers who made the underlying transactions, the liquidity providers of the involved pools, and the hook deployer. | ||
This system creates a mutually beneficial environment where randomness can be sourced in a decentralized manner, while participants (swappers, LPs) are rewarded for contributing to the underlying data used to generate that randomness. | ||
|
||
Developer Experience (DevX) Feedback | ||
Positives: | ||
Easy Integration: | ||
|
||
The hook integrates seamlessly with Balancer V3 pools, making it easy to add decentralized randomness generation to existing pools without disrupting core functionality. | ||
The commit and reveal scheme ensures that randomness is based on real-world, on-chain events, which can improve transparency and trust. | ||
Flexibility: | ||
|
||
By allowing randomness requesters to select how many commits to use, the hook gives users control over the quality and cost of the randomness they are purchasing. | ||
The quadratic pricing model is both flexible and efficient for handling multiple use cases, from small-scale randomness to more robust needs. | ||
Areas for Improvement: | ||
Commit Volume Dependency: | ||
|
||
The randomness system depends on there being enough swaps in the connected pools. If swaps slow down, randomness generation might be delayed. Adding a way to prompt or incentivize more frequent swaps could improve the reliability of the randomness. | ||
Gas Costs: | ||
|
||
Depending on the number of commits aggregated, the process could get gas-intensive, especially when working with a large backlog of commits. There may be opportunities to optimize the gas usage or batch transactions more efficiently to reduce costs for randomness requesters. | ||
LP and Swapper Interaction: | ||
|
||
The way rewards are distributed between swappers and LPs could be made more transparent or configurable by pool managers, allowing custom fee distribution models based on specific pool needs or strategies. | ||
How to Use | ||
Setup: | ||
|
||
Deploy the Commit Miner Hook contract and integrate it with any Balancer V3 pools. | ||
Specify the ERC20 token for payments and rewards. | ||
Generate Randomness: | ||
|
||
Call the requestRandomness() function, specifying the number of commits and the amount of payment. | ||
Fee Distribution: | ||
|
||
Fees are automatically split among swappers, LPs, and the hook deployer based on the specified fee percentages. | ||
License | ||
This project is licensed under the MIT License. | ||
|
||
By using the Commit Miner Hook, you’re contributing to a decentralized randomness generation system while providing yield opportunities to liquidity providers and participants. It's an elegant solution for applications requiring a fair, transparent, and decentralized source of randomness. | ||
|
||
This README outlines the key features, potential use cases, and considerations for developers working with the Commit Miner Hook contract. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works, but could also derive from Ownable and use
owner()
, which does the same thing in a standardized way, and also enables permissioned functions (e.g., maybe the owner can change the fee percentages later).