|
1 | 1 | # ForexSwap |
2 | 2 |
|
3 | | -[gitpod]: https://gitpod.io/#https://github.com/robertleifke/forex-swap |
4 | | -[gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod |
5 | | -[gha]: https://github.com/robertleifke/forex-swap/actions |
6 | | -[gha-badge]: https://github.com/robertleifke/forex-swap/actions/workflows/ci.yml/badge.svg |
7 | | -[foundry]: https://getfoundry.sh/ |
8 | | -[foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg |
9 | | -[license]: https://opensource.org/licenses/MIT |
10 | | -[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg |
| 3 | +> WARNING: This code has not been audited. Treat it as experimental. |
11 | 4 |
|
12 | | -> ⚠️ **WARNING:** This code has not yet been audited. Use at your own risk. |
| 5 | +ForexSwap is a Uniswap v4 hook prototype for FX-style pools. It plugs custom accounting into the v4 swap and liquidity lifecycle so the pool can use a custom curve instead of the default concentrated-liquidity math. |
13 | 6 |
|
14 | | -## Overview |
| 7 | +The implementation in this repo follows the v4 architecture described in the official overview: |
15 | 8 |
|
16 | | -ForexSwap is a Uniswap v4 hook implementation of a [log normal](https://en.wikipedia.org/wiki/Log-normal_distribution) market maker. It's statistical curve that makes liquidity provisioning more passive and capital efficient on frontier FX pairs such as USDC/cNGN. |
| 9 | +- `PoolManager` is the singleton that owns pool state and calls hooks. |
| 10 | +- Hooks are permissioned contracts whose callback surface is encoded into the hook address. |
| 11 | +- Dynamic fees are pool-level behavior in v4, but the fee policy itself is application-defined. |
| 12 | +- Flash accounting is handled by v4; this repo focuses on custom accounting and curve logic. |
17 | 13 |
|
18 | | -## Quick Start |
| 14 | +Official reference: |
| 15 | +- [Uniswap v4 overview](https://docs.uniswap.org/contracts/v4/overview) |
19 | 16 |
|
20 | | -Clone and set up the project: |
| 17 | +## Repo Layout |
21 | 18 |
|
22 | | -```sh |
23 | | -$ git clone https://github.com/robertleifke/forex-swap |
24 | | -$ cd forex-swap |
25 | | -$ bun install |
26 | | -$ forge build |
27 | | -``` |
| 19 | +- [`src/ForexSwap.sol`](src/ForexSwap.sol): main `ForexSwap` hook contract |
| 20 | +- [`tests/ForexSwap.t.sol`](tests/ForexSwap.t.sol): Foundry tests |
| 21 | +- [`script/Anvil.s.sol`](script/Anvil.s.sol): local Anvil deployment and lifecycle script |
| 22 | +- [`script/Deploy.s.sol`](script/Deploy.s.sol): minimal non-local deploy stub |
28 | 23 |
|
29 | | -## Deploy pools |
| 24 | +## Contract Model |
30 | 25 |
|
31 | | -```solidity |
32 | | -IPoolManager poolManager = |
33 | | -ForexSwap forexSwapHook = new ForexSwap(poolManager); |
| 26 | +`ForexSwap` inherits `BaseCustomCurve`, so it opts into v4 custom accounting rather than implementing a standalone pool. The important contract responsibilities are: |
34 | 27 |
|
35 | | -PoolKey memory poolKey = PoolKey({ |
36 | | - currency0: Currency.wrap(address(token0)), |
37 | | - currency1: Currency.wrap(address(token1)), |
38 | | - fee: 0, |
39 | | - tickSpacing: 0, |
40 | | - hooks: IHooks(address(forexSwapHook)) |
41 | | -}); |
42 | | -forexSwapHook.initializePool(poolKey); |
| 28 | +- custom swap math via `_getUnspecifiedAmount`, `_computeAmountOut`, and `_computeAmountIn` |
| 29 | +- owner-managed parameters for `mean`, `width`, and `swapFee` |
| 30 | +- pause and unpause controls |
| 31 | +- hook-local liquidity share accounting for deposits and withdrawals |
43 | 32 |
|
44 | | -forexSwapHook.updateForexSwapParams( |
45 | | - 1.1e18, // mu = 1.1 (10% mean premium) |
46 | | - 2.5e17, // sigma = 0.25 (25% volatility) |
47 | | - 5e15 // swapFee = 0.5% |
48 | | -); |
49 | | -``` |
| 33 | +The current implementation is intentionally conservative. It uses simplified reserve assumptions and approximations rather than a production-ready statistical engine. |
50 | 34 |
|
51 | | -## Testing |
| 35 | +## Prerequisites |
52 | 36 |
|
53 | | -Run comprehensive tests for the ForexSwap implementation: |
| 37 | +This repo expects git submodules to be present. A plain clone is not enough. |
54 | 38 |
|
55 | 39 | ```sh |
56 | | -# Run all tests |
57 | | -$ forge test |
| 40 | +git clone https://github.com/robertleifke/forex-swap |
| 41 | +cd forex-swap |
| 42 | +git submodule update --init --recursive |
| 43 | +bun install |
| 44 | +forge build |
| 45 | +``` |
| 46 | + |
| 47 | +If `forge build` fails with missing imports under `lib/`, the submodules were not initialized correctly. |
58 | 48 |
|
59 | | -# Run with detailed output |
60 | | -$ forge test -vvv |
| 49 | +## Local Development |
61 | 50 |
|
62 | | -# Run gas reporting |
63 | | -$ forge test --gas-report |
| 51 | +The quickest path is a local Anvil deployment: |
64 | 52 |
|
65 | | -# Run specific ForexSwap tests |
66 | | -$ forge test --match-contract ForexSwap -vv |
| 53 | +```sh |
| 54 | +anvil |
| 55 | +forge script script/Anvil.s.sol:AnvilScript \ |
| 56 | + --rpc-url http://localhost:8545 \ |
| 57 | + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ |
| 58 | + --broadcast -vv |
67 | 59 | ``` |
68 | 60 |
|
| 61 | +Or use: |
| 62 | + |
| 63 | +```sh |
| 64 | +./deploy-local.sh |
| 65 | +``` |
69 | 66 |
|
70 | | -## Routing |
| 67 | +The Anvil script deploys: |
71 | 68 |
|
72 | | -### Inverse Normal CDF Implementation |
| 69 | +- a local v4 `PoolManager` |
| 70 | +- the `ForexSwap` hook at a mined address with the required hook permission bits |
| 71 | +- test routers |
| 72 | +- mock ERC20s |
| 73 | +- a sample pool and initial liquidity |
73 | 74 |
|
74 | | -ForexSwap uses the Beasley-Springer-Moro algorithm for computing Φ⁻¹(u): |
| 75 | +## Using the Hook |
| 76 | + |
| 77 | +Example setup: |
75 | 78 |
|
76 | 79 | ```solidity |
77 | | -function _improvedInverseNormalCDF(uint256 u) internal pure returns (int256) { |
78 | | - // Bounded to [-6σ, +6σ] for numerical stability |
79 | | -} |
| 80 | +IPoolManager poolManager = /* deployed v4 PoolManager */; |
| 81 | +ForexSwap forexSwap = new ForexSwap(poolManager); |
| 82 | +
|
| 83 | +PoolKey memory poolKey = PoolKey({ |
| 84 | + currency0: Currency.wrap(address(token0)), |
| 85 | + currency1: Currency.wrap(address(token1)), |
| 86 | + fee: LPFeeLibrary.DYNAMIC_FEE_FLAG, |
| 87 | + tickSpacing: 60, |
| 88 | + hooks: forexSwap |
| 89 | +}); |
| 90 | +
|
| 91 | +poolManager.initialize(poolKey, TickMath.getSqrtPriceAtTick(0)); |
| 92 | +
|
| 93 | +forexSwap.updateLogNormalParams( |
| 94 | + 1.1e18, // mean |
| 95 | + 2.5e17, // width |
| 96 | + 5e15 // 0.5% swap fee |
| 97 | +); |
80 | 98 | ``` |
81 | 99 |
|
82 | | -### Newton-Raphson Iteration |
| 100 | +Two important v4-specific notes: |
83 | 101 |
|
84 | | -For swap calculations, ForexSwap employs iterative solving: |
| 102 | +- The hook address must be mined so its low bits advertise the enabled callbacks. |
| 103 | +- `PoolManager` owns execution. The hook is not a standalone AMM contract. |
85 | 104 |
|
86 | | -```solidity |
87 | | -function _solveExactInputWithLiquidity(...) internal view returns (...) { |
88 | | - // Initial guess using constant product |
89 | | - // Newton-Raphson iteration to solve: Φ⁻¹(x'/L) + Φ⁻¹(y'/L) = k |
90 | | - // Convergence threshold: 1e-6 in WAD precision |
91 | | - // Maximum iterations: 50 |
92 | | -} |
| 105 | +## Testing |
| 106 | + |
| 107 | +```sh |
| 108 | +forge test |
| 109 | +forge test -vvv |
| 110 | +forge test --gas-report |
| 111 | +forge test --match-contract ForexSwapCorrectTest -vv |
93 | 112 | ``` |
94 | | -## 📄 License |
95 | 113 |
|
96 | | -This project is licensed under MIT - see the [LICENSE](LICENSE) file for details. |
| 114 | +## Current Caveats |
| 115 | + |
| 116 | +- The non-local deploy script uses a placeholder `PoolManager` address and is not production-ready. |
| 117 | +- The pricing logic is simplified. It should be treated as a prototype, not a validated FX curve. |
| 118 | + |
| 119 | +## License |
97 | 120 |
|
98 | | ---- |
| 121 | +MIT. See [`LICENSE.md`](LICENSE.md). |
0 commit comments