-
Notifications
You must be signed in to change notification settings - Fork 47
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
Governed Lottery Hook #97
base: main
Are you sure you want to change the base?
Changes from all commits
c65ddc5
08b431b
df3e696
912a875
62df563
15d60fd
0663da6
bb435e2
133183f
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,188 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
||
import { IHooks } from "@balancer-labs/v3-interfaces/contracts/vault/IHooks.sol"; | ||
import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol"; | ||
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol"; | ||
import { | ||
AfterSwapParams, | ||
LiquidityManagement, | ||
SwapKind, | ||
TokenConfig, | ||
HookFlags | ||
} from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol"; | ||
|
||
import { EnumerableMap } from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/EnumerableMap.sol"; | ||
import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol"; | ||
import { VaultGuard } from "@balancer-labs/v3-vault/contracts/VaultGuard.sol"; | ||
import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol"; | ||
|
||
contract GovernedLotteryHook is BaseHooks, VaultGuard, Ownable { | ||
using FixedPoint for uint256; | ||
using EnumerableMap for EnumerableMap.IERC20ToUint256Map; | ||
using SafeERC20 for IERC20; | ||
|
||
// Governance Proposal Struct | ||
struct Proposal { | ||
uint256 proposalId; | ||
string description; | ||
uint64 newSwapFeePercentage; | ||
uint8 newLuckyNumber; | ||
uint256 votesFor; | ||
uint256 votesAgainst; | ||
uint256 votingDeadline; | ||
} | ||
|
||
// State variables for proposals | ||
Proposal[] public proposals; | ||
mapping(uint256 => mapping(address => bool)) public hasVoted; | ||
|
||
// Lottery and fee variables | ||
uint8 public LUCKY_NUMBER = 10; | ||
uint8 public constant MAX_NUMBER = 20; | ||
uint64 public hookSwapFeePercentage; | ||
|
||
EnumerableMap.IERC20ToUint256Map private _tokensWithAccruedFees; | ||
uint256 private _counter = 0; | ||
address private immutable _trustedRouter; | ||
|
||
// Events for governance and fees | ||
event ProposalCreated(uint256 proposalId, string description); | ||
event VoteCast(uint256 proposalId, address voter, bool support); | ||
event ProposalImplemented(uint256 proposalId, uint64 newSwapFeePercentage, uint8 newLuckyNumber); | ||
event LotteryWinningsPaid( | ||
address indexed hooksContract, | ||
address indexed winner, | ||
IERC20 indexed token, | ||
uint256 amountWon | ||
); | ||
|
||
constructor(IVault vault, address router) VaultGuard(vault) Ownable(msg.sender) { | ||
_trustedRouter = router; | ||
} | ||
|
||
// Create a new governance proposal | ||
function createProposal( | ||
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. So in this design, the owner can propose multiple changes, then choose which one(s) to implement. With 5 proposals, if 3 were approved, the owner could alternate between all the approved ones. If only the owner can do this, consider one proposal at a time (perhaps with a variable deadline, constrained by a hard-coded minimum; e.g., min 3 days, but they could pick 3, 5, 7, 14 days, etc.), and a permissionless Another possible design (though more complex), would be some notion of epochs: time periods during which anyone can create a proposal (probably capped to a certain number, like 5, especially if you're using an iterable array), and a permissionless "implement" that would implement the highest scoring proposal (or do nothing if there were no votes), then clear the data and start a new epoch. Pending proposals could only be queried off-chain, which is maybe ok. |
||
string memory description, | ||
uint64 newSwapFeePercentage, | ||
uint8 newLuckyNumber | ||
) external onlyOwner { | ||
proposals.push( | ||
Proposal({ | ||
proposalId: proposals.length, | ||
description: description, | ||
newSwapFeePercentage: newSwapFeePercentage, | ||
newLuckyNumber: newLuckyNumber, | ||
votesFor: 0, | ||
votesAgainst: 0, | ||
votingDeadline: block.timestamp + 7 days | ||
}) | ||
); | ||
|
||
emit ProposalCreated(proposals.length - 1, description); | ||
} | ||
|
||
// Vote on an active proposal | ||
function voteOnProposal(uint256 proposalId, bool support) external { | ||
require(block.timestamp <= proposals[proposalId].votingDeadline, "Voting period is over"); | ||
require(!hasVoted[proposalId][msg.sender], "You have already voted"); | ||
|
||
if (support) { | ||
proposals[proposalId].votesFor += 1; | ||
} else { | ||
proposals[proposalId].votesAgainst += 1; | ||
} | ||
|
||
hasVoted[proposalId][msg.sender] = true; | ||
emit VoteCast(proposalId, msg.sender, support); | ||
} | ||
|
||
// Implement the proposal if it has more votes for than against | ||
function implementProposal(uint256 proposalId) external onlyOwner { | ||
Proposal storage proposal = proposals[proposalId]; | ||
require(block.timestamp > proposal.votingDeadline, "Voting period not ended"); | ||
|
||
if (proposal.votesFor > proposal.votesAgainst) { | ||
hookSwapFeePercentage = proposal.newSwapFeePercentage; | ||
LUCKY_NUMBER = proposal.newLuckyNumber; | ||
|
||
emit ProposalImplemented(proposalId, proposal.newSwapFeePercentage, proposal.newLuckyNumber); | ||
} | ||
} | ||
|
||
// Lottery logic (onAfterSwap remains unchanged for the most part) | ||
function onAfterSwap( | ||
AfterSwapParams calldata params | ||
) public override onlyVault returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) { | ||
uint8 drawnNumber; | ||
if (params.router == _trustedRouter) { | ||
drawnNumber = _getRandomNumber(); | ||
} | ||
|
||
_counter++; | ||
|
||
hookAdjustedAmountCalculatedRaw = params.amountCalculatedRaw; | ||
if (hookSwapFeePercentage > 0) { | ||
uint256 hookFee = params.amountCalculatedRaw.mulDown(hookSwapFeePercentage); | ||
if (params.kind == SwapKind.EXACT_IN) { | ||
uint256 feeToPay = _chargeFeeOrPayWinner(params.router, drawnNumber, params.tokenOut, hookFee); | ||
if (feeToPay > 0) { | ||
hookAdjustedAmountCalculatedRaw -= feeToPay; | ||
} | ||
} else { | ||
uint256 feeToPay = _chargeFeeOrPayWinner(params.router, drawnNumber, params.tokenIn, hookFee); | ||
if (feeToPay > 0) { | ||
hookAdjustedAmountCalculatedRaw += feeToPay; | ||
} | ||
} | ||
} | ||
return (true, hookAdjustedAmountCalculatedRaw); | ||
} | ||
|
||
// Function to set swap fee (can also be changed by governance) | ||
function setHookSwapFeePercentage(uint64 swapFeePercentage) external onlyOwner { | ||
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. As written, only the owner can set the fee (which would need to be validated in production). And if they can just set the fee directly, the voting doesn't really mean anything. (Though it already doesn't, if only the owner can propose any changes.) If you want governance to be able to set it, you would have to implement that (e.g., derive from |
||
hookSwapFeePercentage = swapFeePercentage; | ||
} | ||
|
||
// Pseudo-random number generation | ||
function _getRandomNumber() private view returns (uint8) { | ||
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 have to be an external Oracle or similar thing in production; there's no way to do this safely on-chain. |
||
return uint8((uint(keccak256(abi.encodePacked(block.prevrandao, _counter))) % MAX_NUMBER) + 1); | ||
} | ||
|
||
// Lottery fee and reward logic | ||
function _chargeFeeOrPayWinner( | ||
address router, | ||
uint8 drawnNumber, | ||
IERC20 token, | ||
uint256 hookFee | ||
) private returns (uint256) { | ||
if (drawnNumber == LUCKY_NUMBER) { | ||
address user = IRouterCommon(router).getSender(); | ||
for (uint256 i = _tokensWithAccruedFees.length(); i > 0; i--) { | ||
(IERC20 feeToken, ) = _tokensWithAccruedFees.at(i - 1); | ||
_tokensWithAccruedFees.remove(feeToken); | ||
uint256 amountWon = feeToken.balanceOf(address(this)); | ||
if (amountWon > 0) { | ||
feeToken.safeTransfer(user, amountWon); | ||
emit LotteryWinningsPaid(address(this), user, feeToken, amountWon); | ||
} | ||
} | ||
return 0; | ||
} else { | ||
_tokensWithAccruedFees.set(token, 1); | ||
if (hookFee > 0) { | ||
_vault.sendTo(token, address(this), hookFee); | ||
} | ||
return hookFee; | ||
} | ||
} | ||
|
||
function getHookFlags() public view virtual override returns (HookFlags memory) {} | ||
} | ||
|
||
//this is 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.
I don't think it makes any sense to update the lucky number. Updating the MAX_NUMBER would change the odds, which is meaningful. Maybe allow that to be updated, and generate a new lucky number based on the max (would have to use an Oracle or some other more random means).