ReputationFee

ReputationFee implements 2 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeSwap.

drag to orbit

Uniswap v4 hook · Agent-native

ReputationFee

Charges a counterparty according to how it has behaved, using an on-chain reputation registry, with no signature to present and nothing for a router to forward.

Family
Agent-native
Callbacks
2 of 14
Fee
dynamic
Admin keys
none
Licence
Apache-2.0

How it works

Pools price everyone identically because they cannot tell anyone apart, and the cost of that falls on the people who are cheapest to trade with. A venue that could distinguish its flow would quote the retail order and the arbitrageur differently, which is what every venue that can identify its counterparties actually does. The obstacle in v4 is that a hook does not see the trader.

`beforeSwap` receives the router, not the person who called it, so any scheme keyed on the trader's address needs the trader to sign something and the router to forward it, which means most routers cannot trade the pool at all. This takes the other route. register(myRouter); Thereafter the hook resolves `sender` to that agent with one storage read, asks the registry for its score, and prices the swap between `maxFee` at a score of zero and `minFee` at a perfect score.

No signature, no `hookData`, nothing for a router to support. Flow through an unregistered router is simply unknown and pays `maxFee`, which is the correct default: an unidentified counterparty is priced as the worst one. Spoofing is not possible because the mapping runs from router to agent and only the agent can write its own entry.

Registering a router somebody else also uses means paying for their flow with your reputation, which is a mistake you can only make about yourself. The registry is fixed at deployment and the fee bounds are fixed before the pool exists. There is no admin and no way to re-point the pool at a friendlier scorer once liquidity has arrived.

Prior art

Identity-gated hooks are common (KYC, Civic, VioletID, World ID, PureFi) and they gate: pass or be refused. Loyalty and fidelity hooks discount by volume or tenure, which is a proxy for behaviour rather than a judgement of it. Pricing continuously off an external reputation score, and resolving the trader through a registered router so no signature or hookData is needed, is the contribution here.

Where it does not help

The pool trusts the registry absolutely: a registry that can be bought is a fee schedule that can be bought, and nothing here detects that. Registration is also per router, so an agent that trades through a router it has not registered pays the unknown rate until it registers, and an agent using a shared public router either cannot register it or is subsidising everyone else who uses it.

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.

hook.configure(
    key,
    ReputationFeeHook.Config({
        minFee: /* uint24 */ 0,
        maxFee: /* uint24 */ 0
    })
);

poolManager.initialize(key, startingSqrtPriceX96);

The pool's fee field must be LPFeeLibrary.DYNAMIC_FEE_FLAG. The hook rejects a pool initialized without it, which is the most common integration failure.

Parameters

ParameterTypeUnits
minFeeuint24hundredths of a bip (3000 = 0.30%)
maxFeeuint24hundredths of a bip (3000 = 0.30%)

From TypeScript

npm i @hookforge/sdk

import {getHook, hookAddress, poolKeyFor} from "@hookforge/sdk";

const hook = getHook("reputation-fee");
const key  = poolKeyFor({
  hook: hookAddress("reputation-fee", 8453),   // Base
  currencyA: USDC, currencyB: WETH,
  tickSpacing: 60, dynamicFee: true,
});

What it reverts with

ErrorMeaning
FeeTooLarge(uint24)A fee was configured above the protocol maximum of 100%.
InvalidRegistry()A registry that reports a zero scale cannot be normalised against.
MaxBelowMin()maxFee must be at least minFee; a schedule that rewards a bad score is a configuration error.
NotDynamicFee()The hook was attempted to be initialized with a non-dynamic fee.
PoolAlreadyInitialized()The pool already exists, so its configuration is final.
PoolNotConfigured()The pool was initialized without a configuration for this hook.

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 2, so every deployment of it has an address ending in 0x1080.

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)"    # ReputationFee
cast call $HOOK "specURI()(string)"     # https://reputation-fee.pages.dev/hook.json
cast call $HOOK "hookTags()(string[])"  # agent, reputation, dynamic-fee, erc8004, no-admin

Build, test and deploy

git clone --recurse-submodules https://github.com/nirholas/reputation-fee
cd reputation-fee
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.

Try it

This is the hook running, not a picture of it. Connect a wallet on a chain it is deployed to, or bring the whole stack up locally in one command and use it with no funds and no wallet risk at all.

Loading the demo… if this does not change, JavaScript is blocked and the demo cannot run.

Run the whole thing locally
git clone --recurse-submodules https://github.com/nirholas/reputation-fee
cd reputation-fee

anvil &
forge script script/DeployLocal.s.sol --rpc-url http://127.0.0.1:8545 --broadcast \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

node web/build.mjs && npx serve web/dist

The deploy script writes web/local.json itself and the build merges it, so the page points at the chain you just created without you editing anything. Point a wallet at http://127.0.0.1:8545 and every button on this page works.

Anvil's first account is pre-funded and its key is public by design. Never use it anywhere real.