Private, MEV-resistant swaps on Uniswap v4 using Fhenix Fully Homomorphic Encryption (FHE).
This repo contains a Uniswap v4 hook that validates encrypted orders and records encrypted outputs, plus a simple FHE-enabled ERC20 used in tests and scripts.
- Uniswap v4 hooks via
BaseHookinsrc/TrunkGuardSwapHook.sol - Fhenix CoFHE contracts via
@fhenixprotocol/cofhe-contracts/FHE.sol - Foundry-based tests with CoFHE mocks via
cofhe-foundry-mocks
Files:
-
src/TrunkGuardSwapHook.sol- Imports:
import {FHE, euint128, ebool, Common} from "@fhenixprotocol/cofhe-contracts/FHE.sol"; - Stores encrypted inputs/outputs per pool and order:
mapping(PoolId => mapping(bytes32 => euint128)) public encryptedOrders;mapping(PoolId => mapping(bytes32 => euint128)) public encryptedMinOutputs;mapping(PoolId => mapping(bytes32 => euint128)) public encryptedOutputs;mapping(PoolId => mapping(bytes32 => ebool)) public swapValidations;
- Accepts encrypted inputs via:
submitEncryptedSwap(PoolKey key, euint128 encAmount, euint128 encMinOutput, bytes data)
- Validates homomorphically inside
_beforeSwap:- Computes expected output with
FHE.mul(encAmount, encPrice) - Compares via
FHE.gte(expectedOutput, encMinOutput)to produce anebool
- Computes expected output with
- Records
BalanceDeltain_afterSwap(as encrypted output) - Off-chain decryption flow helpers:
validateSwap,requestOutputDecryption,revealOutput
- Imports:
-
src/HybridFHERC20.sol- Example token used in tests/scripts; standard ERC20-like interface with helper mints
Key concepts:
euint128,eboolare encrypted integer/boolean types- Use
Common.isInitialized(x)to check if encrypted values are set - Use
FHE.asEuint128(value)to create encrypted constants on-chain
Files:
-
src/TrunkGuardSwapHook.sol- Inherits
BaseHookand implements:_beforeSwap(…) internal override returns (bytes4, BeforeSwapDelta, uint24)_afterSwap(…) internal override returns (bytes4, int128)
- Reads pool state via
IPoolManagerandStateLibrary(e.g.getSlot0(key.toId()))
- Inherits
-
Tests and setup use v4-core helpers via
test/utilsand Foundry fixtures.
Hook address permissions:
- Uniswap v4 encodes permissions in the least-significant 14 bits of the hook’s address (see
@uniswap/v4-core/src/libraries/Hooks.sol). - For this hook we require at minimum:
Hooks.BEFORE_SWAP_FLAGHooks.AFTER_SWAP_FLAG- optionally
Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAGif returning deltas
Deploying to a valid address (tests):
- Use
HookMiner.findfromv4-peripheryto generate a CREATE2 salt that yields an address with the proper flags. - Example (see comments in
test/TrunkGuardSwapHook.t.sol):(address hookAddress, bytes32 salt) = HookMiner.find(deployer, flags, type(TrunkGuardSwapHook).creationCode, abi.encode(manager));new TrunkGuardSwapHook{salt: salt}(manager)and assertaddress(hook) == hookAddress
Troubleshooting SenderNotAllowed / HookAddressNotValid:
- These revert when the hook address does not encode the required flags
- Always deploy the hook to an address returned by
HookMiner.find - Ensure you pass the same constructor args to both
findandnew … {salt: …}
trunkGuard/
├─ src/
│ ├─ TrunkGuardSwapHook.sol # Main FHE-enabled Uniswap v4 hook
│ ├─ HybridFHERC20.sol # Simple token used for local tests/scripts
│ └─ interface/
│ └─ IFHERC20.sol
├─ test/
│ ├─ TrunkGuardSwapHook.t.sol # Core tests (Foundry + CoFHE mocks)
│ └─ utils/… # Uniswap v4 testing utilities
├─ script/
│ ├─ 01_CreatePoolAndMintLiquidity.s.sol
│ ├─ 01a_CreatePoolOnly.s.sol
│ ├─ 02_AddLiquidity.s.sol
│ ├─ 03_Swap.s.sol # Simple swap script
│ └─ Anvil.s.sol # Local end-to-end demo
├─ foundry.toml
├─ hardhat.config.ts
└─ remappings.txt
Prereqs:
- Foundry (forge, anvil)
- Node.js (for installing packages if needed)
Install and build:
pnpm install || npm install
forge buildRun tests:
forge test -vvRun local demo on Anvil:
anvil --gas-limit 30000000 &
forge script script/Anvil.s.sol \
--rpc-url http://localhost:8545 \
--broadcastSwap-only demo:
forge script script/03_Swap.s.sol \
--rpc-url http://localhost:8545 \
--broadcast- Tests inherit
CoFheTestfromcofhe-foundry-mocksto simulate FHE flows - You may mock decryption results via the mock task manager helpers if needed
- If you see
SenderNotAllowedorHookAddressNotValidduring pool init:- Switch to
HookMiner.finddeployment flow insetUp()and re-run
- Switch to
- Add richer validation logic in
_beforeSwap - Extend
revealOutputto emit decrypted outputs after authorized reveal - Expand scripts to demonstrate multi-pool scenarios
MIT. See LICENSE.