In Part 1 of this series, we explored the concept of hooks within the Balancer protocol, looked at its architecture, and discussed the possibilities they open up for developers.
Now, in Part 2, we’ll take a practical approach by introducing LoyaltyHook
– a Balancer V3 hook designed to increase user engagement through a tokenized loyalty program.
LoyaltyHook overview
LoyaltyHook
is a Balancer V3 hook that rewards users with loyalty tokens, called LOYALTY
, when they interact with a pool. Whether users add liquidity or perform swaps, they earn LOYALTY
tokens proportional to their activity.
These tokens not only serve as a reward, but also unlock benefits such as reduced swap and exit fees based on the user’s LOYALTY
balance. The more a user participates in the pool, the more they are rewarded, encouraging consistent participation.
By integrating LoyaltyHook into a Balancer pool, liquidity providers and traders are incentivized to remain active, increasing the pool’s liquidity and trading volume. It’s a proactive approach to building a loyal user base and improving the overall ecosystem.
Main features
In order to incentivize user engagement, LoyaltyHook
offers several key features:
Loyalty tokens
Loyalty tokens are the tokenized incentive program:
- Earning rewards: Users earn
LOYALTY
tokens when they add liquidity to the pool or perform swaps within the pool. - Proportional minting: The amount of
LOYALTY
tokens minted is proportional to the value of the transaction (liquidity provided or swap size). - Decay mechanism: To prevent excessive rewards, a decay factor reduces the amount of tokens minted per action over time, encouraging consistent engagement.
Dynamic swap fees
- Fee adjustments: Swap fees are dynamically adjusted based on the user’s LOYALTY balance.
- Greater discounts: Higher LOYALTY balances result in greater fee discounts.
- Encourage retention: Users are incentivised to accumulate and retain LOYALTY tokens to benefit from lower fees.
Implemented in onComputeDynamicSwapFeePercentage
:
function onComputeDynamicSwapFeePercentage(
PoolSwapParams calldata params,
address pool,
uint256 staticSwapFeePercentage
) public view override onlyVault returns (bool success, uint256 dynamicSwapFeePercentage)
Exit fees
- Fee application: An exit fee is applied when users withdraw liquidity to discourage frequent withdrawals.
- Loyalty discounts: Users with sufficient
LOYALTY
tokens will receive discounts on exit fees. At higherLOYALTY
balances levels, exit fees can be significantly reduced or even waived.
Implemented in onAfterSwap
:
function onAfterSwap(
AfterSwapParams calldata params
) public override onlyVault returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) {
Action tracking and decay mechanism
In order to prevent rewarded activities (e.g. trades) from being broken up into smaller chunks in order to multiply LOYALTY
rewards, the rewards decrease with each subsequent action.
- Action count: Tracks the number of actions (swaps or liquidity provisions) performed by a user.
- Decay factor: Adjusts the reward rate over time to encourage sustained engagement rather than sporadic bursts.
- Reset interval: Action counts are reset after a period of time (e.g. 30 days), allowing users to regain higher reward rates.
Tracked by below variables:
// Mapping to track the number of actions per user
mapping(address => uint256) public userActionCount;
// Mapping to track the action reset per user
mapping(address => uint256) public lastActionReset;
// Reset decay after set interval
uint256 public resetInterval = 30 days;
As you remember from part 1, in order to implement the above features, we need to set certain flags in a hook implementation – this is how it looks in LoyaltyHook
.
function getHookFlags() public pure override returns (HookFlags memory) {
HookFlags memory hookFlags;
// apply swap fee discounts
hookFlags.shouldCallComputeDynamicSwapFee = true;
// mint LOYALTY after swap
hookFlags.shouldCallAfterSwap = true;
// must be true for hooks to modify liquidity or swap operations in "after hooks"
hookFlags.enableHookAdjustedAmounts = true;
// apply exit fees after removing liquidity
hookFlags.shouldCallAfterRemoveLiquidity = true;
// mint LOYALTY after adding liquidty
hookFlags.shouldCallAfterAddLiquidity = true;
return hookFlags;
}
Architecture
LoyaltyHook uses a strategy pattern that allows developers to create their own incentive mechanism. This design allows for flexibility in adjusting reward calculations and fee structures without changing the core hook contract.
Let’s take a look at the four main components:
LoyaltyHook contract
The core contract that extends the pool functionality with loyalty incentive mechanisms. LoyaltyHook
delegates calculation tasks to the ILoyaltyRewardStrategy
implementations.
Key responsibilities:
- Tracks user actions and maintains action counts.
- Mints
LOYALTY
tokens based on user interactions. - Dynamically adjusts exchange and exit fees.
- Delegates calculations to ILoyaltyRewardStrategy.
- Access control: Grants the MINTER_ROLE in the LoyaltyToken contract to mint tokens.
ILoyaltyRewardStrategy Interface
Defines the methods that any loyalty reward strategy must implement. This ensures that the LoyaltyHook
can interact with any strategy that adheres to this interface, promoting modularity and extensibility.
interface ILoyaltyRewardStrategy {
function calculateDiscountedFeePercentage(
uint256 baseFeePercentage,
uint256 loyaltyBalance
) external view returns (uint256 discountedFeePercentage);
function calculateExitFees(
uint256[] memory baseAmounts,
uint256 exitFeePercentage,
uint256 loyaltyBalance
) external view returns (uint256[] memory adjustedAmounts, uint256[] memory accruedFees);
function calculateMintAmount(
uint256 baseAmount,
uint256 actionCount
) external view returns (uint256 mintAmount);
}
LoyaltyRewardStrategy contract
Implements the ILoyaltyRewardStrategy
interface and contains the logic for:
- Calculating discounted fees based on
LOYALTY
balance. - Determining the amount of
LOYALTY
tokens to mint. - Computing exit fees with loyalty-based discounts.
Using the strategy pattern, different reward strategies can be swapped in without modifying the LoyaltyHook contract.
The implementation of LoyaltyRewardStrategy
uses a tiered approach to determine fee discounts based on a user’s LOYALTY
token holdings. The implemented strategy includes the following tiers
Tier 1:
- Threshold: ≥ 100 LOYALTY tokens
- Discount: 50% off swap and exit fees
Tier 2:
- Threshold: ≥ 500 LOYALTY tokens
- Discount: 80% discount on swap and exit fees
Tier 3:
- Threshold: ≥ 1000 LOYALTY tokens
- Discount: 90% off swap and exit fees
These tiers encourage users to accumulate LOYALTY
tokens to benefit from greater discounts.
LoyaltyToken contract
An ERC20
token contract that represents the LOYALTY
tokens rewarded to users. It incorporates OpenZeppelin’s AccessControl
for role-based permissions.
- Minting capability: Allows minting of new tokens.
- Access control:
- Uses
AccessControl
to manage roles. - Defines a
MINTER_ROLE
for entities allowed to mint tokens. - The
LoyaltyHook
contract is granted theMINTER_ROLE
to mintLOYALTY
tokens to users.
- Uses
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract LoyaltyToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
// Function to grant minter role (can only be called by admin)
function grantMinterRole(address minter) public onlyRole(DEFAULT_ADMIN_ROLE) {
grantRole(MINTER_ROLE, minter);
}
// Function to revoke minter role (can only be called by admin)
function revokeMinterRole(address minter) public onlyRole(DEFAULT_ADMIN_ROLE) {
revokeRole(MINTER_ROLE, minter);
}
}
Testing
Ensuring the reliability and accuracy of the smart contract implementation is essential. To achieve this, we implemented tests to validate the functionality of the contracts under different scenarios. The test suite is implemented using Foundry in the LoyaltyHookTest
contract.
Test covers a wide range of scenarios to ensure that the LoyaltyHook
behaves as expected. Below are the key areas that were thoroughly tested:
- Swap operations:
- Swaps without any
LOYALTY
balance to ensure standard fees apply. - Swaps with varying
LOYALTY
balances to verify fee discounts are correctly applied. - Verification of correct token balances before and after swaps.
- Swaps without any
- Liquidity provision:
- Adding liquidity and verifying that
LOYALTY
tokens are minted appropriately. - Testing the decay mechanism by performing multiple liquidity additions and checking the reduced minting amounts.
- Adding liquidity and verifying that
- Exit fees:
- Removing liquidity and ensuring that exit fees are calculated based on
LOYALTY
balances. - Confirming that the exit fees are correctly distributed back to the pool or LPs.
- Removing liquidity and ensuring that exit fees are calculated based on
- Action count reset:
- Testing that the action count for users resets after the specified interval (e.g., 30 days).
- Ensuring that after the reset, users receive full minting amounts without accumulated decay.
If you are working on contracts built on top of Balancer v3, remember the base test classes – they make your life A LOT easier by providing a lot of handy helper methods. BaseVaultTest with fake users and their balances is a real life saver – you can provide liquidity, swap and do anything a normal user would do in your code and test user behaviour.
Sample use case
Let’s consider a practical scenario involving Alice, an active trader and liquidity provider within the pool that has LoyaltyHook
integrated.
- Adding liquidity:
- Alice adds liquidity to the pool.
- The
LoyaltyHook
mintsLOYALTY
tokens to her account in proportion to the amount of liquidity she has added. - Her action count increases, and the decay mechanism adjusts her rewards appropriately.
- Performing swaps:
- Alice swaps tokens within the pool.
- She earns additional
LOYALTY
tokens for each swap in proportion to the amount she has swapped. - Her growing
LOYALTY
balance moves her up the loyalty tiers.
- Receiving fee discounts:
- As her
LOYALTY
balance surpasses tier thresholds, she enjoys reduced swap and exit fees. - At Tier 2, she gets an 80% discount, significantly lowering her trading costs.
- As her
- Removing liquidity:
- When Alice decides to withdraw her liquidity, her high
LOYALTY
balance grants her reduced or waived exit fees. - This maximizes her returns and encourages her to continue participating in the future.
- When Alice decides to withdraw her liquidity, her high
Benefits for Alice:
- Lower fees: Enhances her profitability by reducing trading and exit costs.
- Earning rewards: Adds value to her participation through
LOYALTY
tokens. - Incentivized engagement: Aligns incentives with her interest in staying active within the pool.
Roadmap
While the initial implementation of LoyaltyHook
focuses on incentivising user engagement within a liquidity pool, the architecture of tokenized loyalty program allows for some future enhancements:
- Staking mechanisms: Allow users to stake
LOYALTY
tokens to earn additional rewards or exclusive benefits. - Governance participation: Allow
LOYALTY
holders to participate in pool governance decisions, such as adjusting fees or reward parameters. - Cross-platform Integrations: Extend the value of
LOYALTY
tokens by integrating with other DeFi platforms or protocols. - Gamification elements: Introduce milestones, badges or leaderboards to further incentivise user engagement.
- Custom reward strategies: Develop new strategies that are easy to integrate thanks to the modular design.
- Take advantage of more v3 features: Balancer v3 introduces a lot of new mechanisms that could be explored and tied to tokenised rewards, e.g. we could explore the relationship between
LOYALTY
price and PoolCreatorFee -> that could serve as a backing for a loyalty programme.
Conclusion
Our LoyaltyHook
project, submitted for the Balancer Hookathon, demonstrates just a portion of what the Balancer V3 hook framework can achieve. By introducing a tokenized loyalty mechanism, we demonstrated how hooks can be effectively used to increase user engagement and add meaningful incentives within DeFi pools.
Balancer V3’s hook framework provides developers with extensive opportunities to customize and extend pool functionality, from dynamic fee adjustments to custom governance solutions and much more. While our focus was on the LoyaltyHook due to project and time constraints, the potential applications of the hooks are vast and varied. We invite you to experiment with these hooks, realize their full potential, and contribute to the ongoing innovation of the Balancer ecosystem.
If you need any assistance or have questions about implementing Balancer hooks, feel free to reach out to us!
Thank you for joining us for this two-part series on building a custom Balancer hook and exploring our Balancer Hookathon entry. I hope you’ve enjoyed reading it as much as we’ve enjoyed coding it.