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 - NFTLiquidityStakingHook #95

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

0xTaneja
Copy link

NFT Liquidity Staking Hook - BAL Hookathon Submission

An innovative Balancer V3 hook designed to reward liquidity providers with NFTs based on their staking activity. The NFTs unlock DeFi perks, such as fee discounts, governance participation, and boosted rewards, encouraging long-term liquidity provision and engagement.


🪧 Table of Contents


📖 Overview

The NFT Liquidity Staking Hook is a custom Balancer V3 hook designed to boost user engagement by issuing NFTs as rewards to liquidity providers (LPs). These NFTs offer unique DeFi perks, such as:

  • Reduced trading fees for NFT holders.
  • Increased governance rights within the Balancer protocol.
  • Higher rewards for future staking or participation.

By implementing this hook, liquidity pools can increase long-term engagement, attract larger liquidity, and foster community loyalty.


✨ Features

  • NFT Rewards
  • Dynamic Fee Adjustments
  • Cross-Platform Integrations
  • Liquidity staking mechanism
  • NFT minting and upgrading
  • Tiered reward system
  • Fee discounts
  • Yield boosting
  • Governance integration

🎖 NFT Tiers and Perks

The NFTs awarded through the NFT Liquidity Staking Hook are tiered to reflect a provider’s activity:

Tier Threshold Perks
Bronze ≥ 1 month liquidity staking 10% discount on swap fees, entry into governance ,Tier 1
Silver ≥ 3 months liquidity staking 25% discount on swap fees, governance rights, 10% bonus on staking rewards ,Tier 2
Gold ≥ 6 months liquidity staking 50% discount on swap fees, governance power boost, 25% bonus on staking rewards,Tier 3

🛠 Architecture

XLDDRzim3BthLn2zvxHRC0pjihORjWv1FdgPVU1irXLYYwuafyk6_lUHjkCesue1FaXyJq-FVF9bYEXZrzJmzMRpnJvrjhJni8wuiwxwoBo3AryysaX5x3mPWqLa3zj0tfWMYnrM11VliKPNa8VxDt1TS3Y4ICq5_380tr9ioLpdvYKv-TgbwrJY5L8E640N8-UE8fOOCTVzYfxHbnlUaSj7_

NFTLiquidityStakingHook Contract

  • The core contract that integrates with the liquidity pool, issuing NFTs and managing reward logic.
  • Tracks user staking activity, including how much liquidity is provided and for how long.
  • Implements the logic for calculating when and what type of NFT is minted.
  • Calculates the perks associated with each NFT tier, such as fee discounts and reward boosts.

NFTGovernor Contract

  • Governance Framework: The contract extends OpenZeppelin's Governor, GovernorCountingSimple, and GovernorVotes
    contracts, providing a basic governance structure.
  • Token-based Voting: It uses an IVotes compatible token for voting power, initialized in the constructor.
  • Voting Parameters:
  • Voting Delay: Set to 1 day (the time between proposal creation and voting start).
  • Voting Period: Set to 1 week (the duration of the voting phase).
  • Quorum: Set to 1000e18 tokens (the minimum number of votes required for a proposal to pass).

NFTMetadata

  • Provides a flexible system for representing different tiers of liquidity staking positions as NFTs
  • Allows for easy customization of tier names and colors
  • Generates visual representations (SVG images) of the NFTs directly on-chain.
  • Creates standardized metadata for each NFT, making them compatible with NFT marketplaces and wallets

RewardToken

  • Implements an ERC20 token named "Reward Token" with symbol "RWD"
  • Has a designated stakingHook address
  • Allows minting of new tokens, but only by the stakingHook address

🚀 Usage

Deploying the Contracts

   NFTMetadata nftMetadata = new NFTMetadata();
   RewardToken rewardToken = new RewardToken(address(stakingHook));
   NFTGovernor governor = new NFTGovernor(IVotes(address(stakingHook)));
   NFTLiquidityStakingHook stakingHook = new NFTLiquidityStakingHook(vault, factory, "StakingNFT", "SNFT", 
   address(nftMetadata));

Setup

   stakingHook.setRewardToken(address(rewardToken));
   stakingHook.setGovernor(address(governor));
   // Set tier benefits
   stakingHook.setFeeDiscounts([1, 2, 3], [100, 200, 300]);

Integrating with a Pool

  • Attach the NFTLiquidityStakingHook to a Balancer pool by interacting with the Vault.
  • Add liquidity to Balancer pool
  • NFTs are automatically minted based on staking amount and duration
  • Claim rewards: stakingHook.claimRewards(poolAddress)

💡 Example Use Case

Scenario: Imagine Bob is a liquidity provider who deposits 50 ETH into a Balancer pool with the NFT Liquidity Staking Hook enabled.

  1. Bob Stakes Liquidity: Bob’s liquidity is tracked by the hook.
  2. Bob Earns a Silver NFT: After 3 months, Bob’s staking activity meets the threshold for a Silver NFT, which is automatically minted to his wallet.
  3. Bob Enjoys Perks: With the Silver NFT, Bob enjoys a 25% discount on swap fees and 10% bonus on future staking rewards.
  4. Increased Governance Power: The Silver NFT also grants Bob voting power in protocol decisions, increasing his influence within Balancer.

🏦 Benefits

For Users

  • Exclusive Rewards: NFTs provide users with perks that enhance their trading and staking experience.
  • Fee Reductions: Lower fees based on NFT tier, improving profitability.
  • Engagement Incentives: Encourages long-term participation by offering evolving rewards.

For Pool Creators

  • Increased Liquidity: NFT rewards incentivize users to provide more liquidity for longer periods.
  • Community Building: NFT-based governance strengthens the relationship between users and the protocol.

🧪 Testing

The testing suite for the NFTLiquidityStakingHook was built using Foundry and covers scenarios like:

  • Staking and NFT Minting: Verifies correct NFT issuance upon liquidity provision
  • Checks initial tier assignment and staking info:
  • Tier Upgrades:Tests NFT tier upgrades based on staking duration
  • Ensures correct tier-based benefits (fee discounts, voting power, yield boosts)
  • Reward Mechanism:Validates reward accrual and claiming process
  • Tests yield boost calculations for different tiers
  • Governance Integration: Checks proposal creation and voting mechanisms
  • Verifies execution of passed proposals
  • Other Test Such as UnStaking, NFTMetadata Generation , etc.

🛠 Development Notes

  • Extensibility: The hook’s design allows for future integration of additional perks, like staking multipliers or exclusive governance rights.
  • Strategy Pattern: By separating the reward logic from the core hook contract, different reward strategies can be easily implemented.

📌 Roadmap

Future improvements to the NFT Liquidity Staking Hook include:

  • Staking Multipliers: Higher rewards for users who lock their NFTs for a set period.
  • Governance Enhancements: Expanded governance rights for NFT holders, allowing them to propose changes to the protocol.
  • Gamification: Introduce leveling systems, badges, and milestones to further incentivize engagement.

📜 License

This project is licensed under the MIT License.


Thank you for considering the NFT Liquidity Staking Hook for the BAL Hookathon. I look forward to your feedback!

Copy link

vercel bot commented Oct 14, 2024

@0xTaneja is attempting to deploy a commit to the Matt Pereira's projects Team on Vercel.

A member of the Team first needs to authorize it.

@0xTaneja
Copy link
Author

0xTaneja commented Oct 16, 2024

Hey @rakpawel or @MattPereira , could you please review this when you get a chance? Thanks!

Comment on lines 289 to 297
function onAfterRemoveLiquidity(
address router,
uint256[] memory amountsOutScaled18,
uint256 bptAmountIn,
uint256[] memory balancesScaled18,
bytes memory userData
) external returns (bool success) {
address user = router;
address pool = msg.sender;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help us understand why you're setting user to equal the router address? Wouldn't that mean you always have the same "user" which would be problematic for the _stakingInfoMap logic below?

Copy link
Member

@MattPereira MattPereira Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the msg.sender ( in the context of who is calling the hook ) is actually the vault and not the pool. I think pool contracts mainly just do math and return results to the vault

Copy link
Member

@MattPereira MattPereira Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend checking out our hooks docs and hooks video

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help us understand why you're setting user to equal the router address? Wouldn't that mean you always have the same "user" which would be problematic for the _stakingInfoMap logic below?

Thank you for pointing this out. You're correct, and I've updated the code. In the onAfterRemoveLiquidity function, router is indeed the user's address in this context. I've left it as address user = router; for clarity, but it's no longer problematic as it correctly represents the user for each transaction.
I've updated the code to correctly identify the pool address. Now, instead of using msg.sender, we're using IPoolInfo(msg.sender).getPoolAddress() to get the actual pool address.

Comment on lines 169 to 182
function simulateAddLiquidity(address user, address poolAddress, uint256 amount) internal {
vm.prank(address(0));
(bool success, ) = address(hook).call(
abi.encodeWithSignature(
"onAfterAddLiquidity(address,address,uint256,uint256,bytes)",
user,
poolAddress,
amount,
amount,
""
)
);
require(success, "onAfterAddLiquidity call failed");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is probably not an ideal way to simulate adding liquidity because the caller is the zero address instead of the vault, no tokens are being moved, and also the function signature doesnt seem to match the onAfterAddLiquidity from the v3 interface for pool hooks

https://github.com/balancer/balancer-v3-monorepo/blob/1af8adb8da472d2a03016092ec3785d7b93db348/pkg/interfaces/contracts/vault/IHooks.sol#L135-L144

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I think you want to actually add liquidity by calling router.addLiquidity... similar to how the FeeTakingHookExample test does it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I think you want to actually add liquidity by calling router.addLiquidity... similar to how the FeeTakingHookExample test does it

You are absolutely right ! i watched hook videos and started to build by my own . i got errors , resolved them daily. but then i thought why not follow example codes so read them and build by my own!!
Thank you for this feedback. I've completely revamped the simulateAddLiquidity function in our test file. It now uses the vault as the caller instead of the zero address, and the function signature matches the onAfterAddLiquidity from the v3 interface.

Comment on lines 47 to 66
mapping(uint256 => uint256) public nftTierToFeeDiscount;
mapping(uint256 => uint256) public nftTierToVotingPower;
mapping(uint256 => uint256) public nftTierToYieldBoost;
mapping(uint256 => mapping(address => uint256)) private _votePowerCheckpoints;
mapping(uint256 => uint256) private _totalSupplyCheckpoints;
uint256 private _currentCheckpoint;

mapping(address => mapping(uint256 => uint256)) private _ownedTokens;
mapping(address => uint256) private _ownedTokensCount;

uint256 public constant COOLDOWN_PERIOD = 7 days;
uint256 public constant UPGRADE_COOLDOWN_PERIOD = 7 days;

uint256 public constant BRONZE_TIER_THRESHOLD = 30 days;
uint256 public constant SILVER_TIER_THRESHOLD = 90 days;
uint256 public constant GOLD_TIER_THRESHOLD = 180 days;

uint256 public constant BRONZE_TIER_AMOUNT = 1000 ether;
uint256 public constant SILVER_TIER_AMOUNT = 5000 ether;
uint256 public constant GOLD_TIER_AMOUNT = 10000 ether;
Copy link
Member

@MattPereira MattPereira Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might consider using an enum type to represent the different nft tiers and grouping related data about each tier into a struct

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might consider using an enum type to represent the different nft tiers and grouping related data about each tier into a struct

Thank you for this excellent suggestion. I've implemented it in the code. We now use an enum NFTTier to represent the different tiers, and a struct TierInfo to group related data about each tier.

I've made these changes and updated all relevant parts of the contract and test files to ensure consistency with these new implementations. Please let me know if you'd like me to clarify or expand on any of these changes.

@0xTaneja
Copy link
Author

0xTaneja commented Oct 18, 2024

I have tried to correct things which you have pointed . Also @MattPereira let implementation be aside ! What Do You think about my Idea ??

@MattPereira MattPereira self-requested a review October 22, 2024 22:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants