Uniswap v4 hook that tracks how much native ETH flows into and out of one pool.
The hook exposes cumulative inflow, cumulative outflow, and per-epoch breakdowns on-chain. Off-chain tools (treasury monitors, reserve alerts, analytics jobs) can read the counters directly or index the events without reimplementing swap parsing.
Built with Foundry. Depends on v4-core and v4-periphery.
Every swap on a native ETH pool triggers afterSwap. The hook reads the executed BalanceDelta, finds the ETH leg, and updates:
- All-time counters:
totalEthInflow,totalEthOutflow - Epoch counters:
epochEthInflow[epochId],epochEthOutflow[epochId] - Events:
SwapTrackedon every ETH swap,EpochAdvancedwhen a new epoch window opens
Net flow is never stored as a single signed value. It is derived on read via getNetFlow().
| Term | Meaning |
|---|---|
| Inflow | ETH enters the pool. The trader sells ETH for the paired token. |
| Outflow | ETH leaves the pool. The trader buys ETH with the paired token. |
| Net flow | Inflow minus outflow (computed in a view function). |
Native ETH is detected by address(0) on either currency0 or currency1. The hook does not hardcode ETH to token0.
v4 passes afterSwap a BalanceDelta from the swapper's perspective:
| ETH delta sign | Pool effect | Our label |
|---|---|---|
| Negative | Swapper pays ETH in | Inflow |
| Positive | Swapper receives ETH out | Outflow |
This matches how v4 routers settle swaps: negative delta means the swapper pays (settle), positive means they receive (take).
Time is split into fixed windows for scoped totals:
- Set
epochLengthat deploy (default 1 day if you pass0) epochId = block.timestamp / epochLength(no external oracle)- Each epoch keeps its own inflow/outflow mappings, plus separate all-time totals
- The first swap in a new epoch emits
EpochAdvanced
| Function / variable | Returns |
|---|---|
totalEthInflow |
All-time ETH into the pool |
totalEthOutflow |
All-time ETH out of the pool |
getNetFlow() |
All-time net as int256 |
getCurrentEpoch() |
Active epoch id |
getEpochFlows(epochId) |
(inflow, outflow) for one epoch |
SwapTracked (every ETH swap):
event SwapTracked(
uint256 indexed epochId,
FlowDirection direction, // Inflow or Outflow
uint256 amount,
address indexed sender,
uint256 timestamp
);EpochAdvanced (first swap after a boundary):
event EpochAdvanced(
uint256 indexed previousEpochId,
uint256 indexed newEpochId,
uint256 timestamp
);src/
NetEthFlowHook.sol Hook contract
test/
NetEthFlowHook.t.sol Foundry tests
lib/
v4-periphery/ Uniswap v4 (includes nested v4-core)
forge-std/
foundry.toml Compiler + remappings
remappings.txt Import paths
Clone with submodules, then install Foundry deps:
git clone --recurse-submodules https://github.com/cipherEncrypt/Net-ETH-Flow-Hook.git
cd Net-ETH-Flow-Hook
forge install foundry-rs/forge-stdIf you already cloned without submodules:
git submodule update --init --recursive
forge install foundry-rs/forge-std# compile
forge build
# format
forge fmt
# run tests
forge test -vv
# gas report
forge test --gas-reportAll 6 tests should pass:
- ETH-in swap increments inflow
- ETH-out swap increments outflow
- Multiple swaps update cumulative and epoch totals
- Epoch rollover keeps old epoch data readable
- First swap does not emit spurious
EpochAdvanced - Fuzz on inflow amounts
v4 encodes hook permissions in the contract address. You cannot deploy with a plain new NetEthFlowHook(...).
Use CREATE2 with HookMiner to mine a salt where the address matches afterSwap: true. See test/NetEthFlowHook.t.sol for the pattern:
uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG);
bytes memory args = abi.encode(poolManager, epochLengthSeconds);
(, bytes32 salt) = HookMiner.find(deployer, flags, type(NetEthFlowHook).creationCode, args);
NetEthFlowHook hook = new NetEthFlowHook{salt: salt}(poolManager, epochLengthSeconds);This repo uses the current v4 API:
- No
BaseHookorgetHookPermissions()in the installed deps - Permissions live in
Hooks.Permissions, checked viavalidateHookPermissions()in the constructor - Our readable wrapper is
hookPermissions()on the contract
This is a PoC. Intentionally out of scope:
- Multi-pool aggregation
- External price feeds
- Fee adjustment logic
beforeSwaptracking- Deployment scripts
- Dashboard / frontend
One pool, one hook, afterSwap only.
foundry.toml matches v4 where it matters:
- Solidity
0.8.26 - EVM
cancun via_ir = true
Remappings point @uniswap/v4-core at the nested copy inside v4-periphery/lib/v4-core/ (the top-level lib/v4-core submodule may lag behind).
MIT for the hook contract. Uniswap libraries under lib/ keep their own licenses.