A yield enhancement hook for PancakeSwap v4 that maximizes returns for LPs by deploying idle liquidity to Yearn V3 vaults while maintaining configurable buffer ratios for active trading.
struct YieldVaults {
address vault0;
uint256 amount0TotalBalance; // Total tracked balance for token0
uint256 amount0IdleBalance; // Unmoved balance for token0
uint256 vault0ShareBalance; // Vault shares for token0
address vault1;
uint256 amount1TotalBalance; // Total tracked balance for token1
uint256 amount1IdleBalance; // Unmoved balance for token1
uint256 vault1ShareBalance; // Vault shares for token1
}
minBufferBps
: Minimum ratio of idle funds (2000 = 20%)targetBufferBps
: Target ratio for idle funds (4000 = 40%)maxBufferBps
: Maximum ratio of idle funds (5000 = 50%)hookManager
: Address authorized to modify hook parameters
function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata)
external override returns (bytes4)
- Initializes YieldVaults structure for the pool
- Sets up vault associations for both tokens
- Records initial zero balances
function beforeAddLiquidity(address, PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata params, bytes calldata)
external override returns (bytes4)
- Checks if liquidity is being added within current tick range
- Distributes accumulated yield to active LPs if in range
function afterAddLiquidity(address, PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata)
external override returns (bytes4, BalanceDelta)
- Ensures buffer ratios are maintained
- Deploys excess funds to vaults if needed
function beforeRemoveLiquidity(address, PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata params, bytes calldata)
external override returns (bytes4)
- Checks if liquidity being removed is in current tick range
- Distributes yield if removing active liquidity
function afterRemoveLiquidity(address, PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata, BalanceDelta balanceDelta, bytes calldata)
external override returns (bytes4, BalanceDelta)
- Ensures sufficient buffer remains after removal
- Withdraws from vaults if needed
function beforeSwap(address, PoolKey calldata key,
ICLPoolManager.SwapParams calldata params, bytes calldata)
external override returns (bytes4, BeforeSwapDelta, uint24)
- Checks if swap will cross a tick boundary
- Distributes yield if crossing tick
- Ensures sufficient buffer for swap
function afterSwap(address, PoolKey calldata key,
ICLPoolManager.SwapParams calldata, BalanceDelta balanceDelta, bytes calldata)
external override returns (bytes4, int128)
- Rebalances buffers based on swap impact
- Withdraws or deposits to maintain target ratios
function _distributeYield(PoolKey calldata key) private
returns (uint256 donationAmount0, uint256 donationAmount1)
- Calculates yield since last distribution for each token:
- For each token with a configured vault:
- Total yield = currentIdleBalance + convertedVaultShares - totalTrackedBalance
- For each token with a configured vault:
- Handles withdrawals if yield exceeds idle balance:
- Withdraws required amount from vault to hook
- Updates vault share balance
- Updates idle balances
- Calls
poolManager.donate()
to distribute yield - Updates vault state
function _ensureSwapBuffer(PoolKey calldata key, int256 amount0, int256 amount1)
private returns (BalanceDelta)
- For each token with configured vault:
- Calculates current buffer ratio using MAX_BPS (10,000)
- Checks if ratios within bounds (min/max buffer BPS)
- Ensures sufficient funds for pending operation
- Adjusts funds available for deposit to maintain target ratio
- Returns the hook's
BalanceDelta
to achieve the above
function takeFunds(PoolKey calldata key) external
- Locks the vault using vault.lock()
- Calls internal implementation via encoded function call
- Accessible by any external caller (permissionless)
function _takeFunds(PoolKey calldata key) public selfOnly
- Can only be called through vault lock mechanism
- For each token with configured vault:
- Calls vault.take() to move idle token balance
- Deposits full idle balance into Yearn vault
- Updates vault share balance
- Resets idle balance to zero
- Updates YieldVaults struct with new balances
function _checkTickCross(PoolKey calldata key,
ICLPoolManager.SwapParams calldata params) private returns (bool)
- Gets current tick and price from slot0
- Gets tick info for current tick
- Calculates next price using SqrtPriceMath:
- Uses getNextSqrtPriceFromInput for positive amounts
- Uses getNextSqrtPriceFromOutput for negative amounts
- Converts price to tick using TickMath
- Returns true if tick changes
-
Balance Tracking
- Maintain accurate accounting of idle and vault-deployed funds
- Track vault shares separately from underlying assets
-
Buffer Management
- Enforce minimum buffer constraints
- Prevent excessive vault withdrawals
- Handle slippage on vault operations
-
Yield Distribution
- Only distribute to active liquidity positions
- Ensure accurate yield calculations
- Handle potential vault share value fluctuations
-
Storage Access
- Cache YieldVaults struct in memory when possible
- Batch vault operations to minimize calls
-
Yield Distribution
- Only distribute yield on tick crosses or LP changes
- Optimize vault share calculations
-
Buffer Management
- Only rebalance when significantly out of range
- Combine vault operations when possible
-
Yield Calculation Tests
- Verify accurate yield tracking
- Test distribution proportions
- Validate vault share conversions
-
Buffer Management Tests
- Test ratio maintenance
- Verify bounds enforcement
- Check rebalancing triggers
-
Integration Tests
- Full lifecycle with PancakeSwap v4 pools
- Multiple LP scenarios
- Complex swap patterns
- Tick crossing events
-
Q: Does an LP interact with the hook to add / remove positions or the poolManager? A: LPs interact with the poolManager
-
Q: How is the rebalancing of positions done if user directly interacts with the poolManager? A: Rebalancing is done when required by checking if it's necessary in the after hooks for swap, addLiq, and removeLiq
-
Q: Why is the yield paid through donate to active tick because the additional yield is earned through funds in inactive tick? A: Yield is given only to the active tick to encourage users to provide liq at-the-money.
-
Q: If LPs are allowed to have custom range, how does the hook manage each individual position while rebalancing? A: The hook is agnostic to individual positions. They can provide (via the poolManager) to the range of their choice. Since yield is distributed through the
donate
method, yield is automatically distrubted to LPs alongside their fees. -
Q: What does idle funds / targetBuffer mean? Are these funds deployed in AMM across a range or just left idle to process withdrawals? A: The targetBuffer are funds that are left as base asset to provide for low gas cost swaps. Ideally, most swaps will be serviced without having to withdraw from the yield vaults. Withdraws may also be serviced by the before, but the goal of minimizing gas costs is deemed less important when working with modifyLiq operations.