EpochRebalance implements 0 of the fourteen Uniswap v4 callbacks: none.
drag to orbit
Uniswap v4 hook · Time
EpochRebalance
An index whose whole future is fixed before anybody deposits: the pool's target allocation glides along a schedule written at deployment, and nobody, including the deployer, can change it afterwards.
- Family
- Time
- Callbacks
- 0 of 14
- Fee
- static
- Admin keys
- none
- Licence
- Apache-2.0
How it works
A weighted pool is already an index fund. Holding `x^w * y^(1-w)` constant keeps the value split at `w` to `1-w` without anybody trading, because arbitrageurs restore the ratio for free every time the price moves. That result is Balancer's and it is why weighted pools are described as self-rebalancing.
What a real index also needs is reconstitution: the target itself changes. Balancer does this with an owner call that starts a gradual weight update, and that is the part worth improving on, for two reasons. The first is trust.
A provider deposits into a 60/40 pool and the owner can make it 20/80 next week. Whatever the governance around that key, the provider's exposure is not theirs to control, and the pool's terms are a promise rather than a property. The second is the rebalance itself.
A weight change is a trade the pool must do, and if it happens as one step the whole trade is available at one price in one block, which is the most arbitrageable shape a rebalance can have. Every index that reconstitutes on a known date pays for this, and it has a name: index front-running. This hook fixes both by fixing the schedule.
Weight checkpoints are set at construction and interpolated linearly between, so the pool's allocation on any future date is computable by anyone from the moment it exists. A provider knows what they are joining for the whole life of the pool. And because the weight moves continuously rather than in a step, the rebalancing flow is spread across the glide instead of concentrated into one block: there is no single moment worth racing to, because at every moment only an instant's worth of the trade is available.
The same mechanism is a liquidity bootstrapping pool when the schedule is two points and the first weight is lopsided, so this covers that case without a separate contract.
Prior art
Weighted pools as self-rebalancing indices are Balancer's, as are gradual weight updates, which are triggered by an owner. Liquidity bootstrapping pools are the two-point case. Making the entire weight schedule immutable and set before the first deposit, so the pool's future allocation is a property a provider can verify rather than a promise a key holder can revise, is the contribution here, along with the observation that a continuous glide removes the single arbitrageable print a stepped reconstitution creates.
Where it does not help
The schedule cannot respond to anything. An index that needs to react to a delisting, a merger or a depeg cannot be expressed here, and pretending otherwise by picking a schedule that happens to look right is worse than using a pool with a keeper. The glide also does not eliminate rebalancing cost, it spreads it: the pool still trades into the new weight and still pays for that, it simply does not hand the whole trade to one participant in one block.
Using it
Uniswap v4 removed hookData from initialize, so per-pool parameters arrive out of band.
Fix them for a pool key whose pool does not exist yet, then initialize. Nobody can change them afterwards,
including you.
poolManager.initialize(key, startingSqrtPriceX96);
Parameters
This hook takes no per-pool configuration.
From TypeScript
npm i @hookforge/sdk
import {getHook, hookAddress, poolKeyFor} from "@hookforge/sdk";
const hook = getHook("epoch-rebalance");
const key = poolKeyFor({
hook: hookAddress("epoch-rebalance", 8453), // Base
currencyA: USDC, currencyB: WETH,
tickSpacing: 60,
});
What it reverts with
| Error | Meaning |
|---|---|
AlreadyInitialized() | Hook was already initialized. |
AmountTooSmall() | A deposit was too small to mint any shares, or a withdrawal too small to return anything. |
ERC20InsufficientAllowance(address,uint256,uint256) | Indicates a failure with the spender’s allowance. Used in transfers. |
ERC20InsufficientBalance(address,uint256,uint256) | Indicates an error related to the current balance of a sender. Used in transfers. |
ERC20InvalidApprover(address) | Indicates a failure with the approver of a token to be approved. Used in approvals. |
ERC20InvalidReceiver(address) | Indicates a failure with the token receiver. Used in transfers. |
ERC20InvalidSender(address) | Indicates a failure with the token sender. Used in transfers. |
ERC20InvalidSpender(address) | Indicates a failure with the spender to be approved. Used in approvals. |
ExpiredPastDeadline() | A liquidity modification order was attempted to be executed after the deadline. |
InsufficientInitialLiquidity() | The first deposit must exceed the permanently locked minimum. |
InsufficientReserves() | The pool cannot fill this swap without emptying the side being bought. |
InvalidFee() | The fee must be below 100%. |
InvalidNativePayer(address) | The native currency was settled on behalf of a payer other than the contract paying it. |
InvalidNativeValue() | Native currency was not sent with the correct amount. |
InvalidSchedule() | The schedule was empty, too long, out of order, or carried a weight outside the usable band. |
LiquidityOnlyViaHook() | Liquidity was attempted to be added or removed via the PoolManager instead of the hook. |
NoLiquidity() | A quote was requested against an empty pool. |
PoolNotInitialized() | Pool was not initialized. |
SafeERC20FailedOperation(address) | An operation with an ERC-20 token failed. |
TooMuchSlippage() | Principal delta of liquidity modification resulted in too much slippage. |
The callbacks it claims
Uniswap v4 reads a hook's permissions from the low fourteen bits of its own address, which is why deploying one
means mining a CREATE2 salt. This hook claims 0, so every deployment of it has an address ending
in 0x0.
- beforeInitialize
- afterInitialize
- beforeAddLiquidity
- afterAddLiquidity
- beforeRemoveLiquidity
- afterRemoveLiquidity
- beforeSwap
- afterSwap
- beforeDonate
- afterDonate
- beforeSwapReturnsDelta
- afterSwapReturnsDelta
- afterAddLiquidityReturnsDelta
- afterRemoveLiquidityReturnsDelta
It says what it is, on-chain
Nothing about a hook's address tells an indexer, a wallet, a router or an agent what the pool does, which is why
hook discovery today is a curated list. This hook answers for itself, in one eth_call, with no
registry in the loop.
cast call $HOOK "hookName()(string)" # EpochRebalance
cast call $HOOK "specURI()(string)" # https://epoch-rebalance.pages.dev/hook.json
cast call $HOOK "hookTags()(string[])" # curve, index, rebalancing, schedule, no-admin
Build, test and deploy
git clone --recurse-submodules https://github.com/nirholas/epoch-rebalance
cd epoch-rebalance
forge build && forge test
# Dry run: mines the salt, prints the address, sends nothing.
forge script script/Deploy.s.sol --rpc-url $RPC_URL
# For real.
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify
Status
Unaudited. Built to an audited shape, on OpenZeppelin's audited hook bases, and tested against
a real PoolManager. No third party has reviewed it. Read "where it does not help" above before
putting money behind it. Not affiliated with Uniswap Labs.