AgentHook
contracts/src/agents/AgentHook.sol
Uniswap V4 hook shared across all agent pools. One hook deployment handles every agent launched by AgentFactory. Each pool has its own PoolState keyed by PoolId.
Hook flags
The hook must be deployed at an address where the lowest 14 bits equal 0x0AC0:
BEFORE_ADD_LIQUIDITY bit 11 = 0x0800
BEFORE_REMOVE_LIQUIDITY bit 9 = 0x0200
BEFORE_SWAP bit 7 = 0x0080
AFTER_SWAP bit 6 = 0x0040
Required: address & 0x3FFF == 0x0AC0This is enforced by mining a CREATE2 salt before production deployment.
PoolState
struct PoolState {
address agentToken; // the agent's ERC-20 trading token
uint64 launchBlock; // fee decay starts here
uint24 baseFee; // fee after full decay (e.g. 3_000 = 0.3%)
uint24 maxFee; // fee at launch (e.g. 500_000 = 50%)
uint32 decayBlocks; // blocks until fee reaches baseFee
int24 currentFloorTick;
}Dynamic fee
Fee decays linearly from maxFee to baseFee over decayBlocks:
elapsed = block.number - state.launchBlock
fee = maxFee - (maxFee - baseFee) * min(elapsed, decayBlocks) / decayBlocksSet in beforeSwap via V4's dynamic fee mechanism (LPFeeLibrary.DYNAMIC_FEE_FLAG).
LP restrictions
beforeAddLiquidity and beforeRemoveLiquidity revert for all senders except the hook itself. Only AgentHook can add or remove liquidity — external LP providers cannot participate. All liquidity is one-sided (AgentToken only) at launch, and the hook manages the position through rebalance.
collectFees(PoolKey key)
Pulls accrued LP fees from the V4 PoolManager and routes them:
DIEM fees (buy-side):
poolManager.take(diem, address(this), amount)
diem.approve(pool, amount)
pool.enterFor(agentToken, amount) ← locked forever
AgentToken fees (sell-side):
poolManager.take(agentToken, address(this), amount)
agentToken.burnForYieldTo(amount, agentToken.owner())
→ AT burned (deflationary)
→ YieldToken minted to agentWalletrebalance(PoolKey key)
Shifts the LP floor tick upward when circulating supply has decreased enough that the previous floor is no longer reachable. Extracts "dead DIEM" — DIEM that can never be bought back because the AT that would purchase it has been burned.
1. Compute price floor from circulating supply:
sqrtFloor = L * sqrtCurrent / (L + circulatingSupply * sqrtCurrent / Q96)
2. If floor tick ≤ currentFloorTick: nothing to do.
3. Remove LP [currentFloorTick, TICK_UPPER]
4. Re-add LP [floorTick, TICK_UPPER] (same liquidity)
5. Net DIEM from the shift = dead DIEM:
pool.enterFor(agentToken, deadDiem) ← locked forever
6. Net AgentToken from the shift (rare):
agentToken.burnForYieldTo(netAT, agentToken.owner())The rebalance is permissionless — any keeper can call it once the floor has moved.
addInitialLiquidity(PoolKey key, int256 liquidityDelta)
Called once by AgentFactory at launch. Adds the full initial AgentToken supply as one-sided LP (no DIEM required). Only callable by the registered factory.