Skip to content

Latest commit

 

History

49 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BetterSwap

A Uniswap-V2-style AMM in Solidity / Foundry — built from scratch as a portfolio project, with a focus on faithful reproduction of V2 mechanics and on-chain invariant safety.

Stack: Solidity ^0.8.29 · Foundry · OpenZeppelin Status: actively developed · core protocol (Pool + Factory + Router + WETH) implemented and tested · 86 tests passing


Overview

BetterSwap implements the core building blocks of a Uniswap-V2-style decentralized exchange:

  • A constant-product AMM pool with the canonical 0.3% trading fee, a TWAP price oracle, and an optional protocol fee
  • A factory that deploys and tracks one pool per token pair and controls the protocol-fee switch
  • A router for adding/removing liquidity, multi-hop swaps, and native-ETH swaps via WETH
  • A fuzz test that asserts the k = x · y invariant survives random swap sequences

This is a learning project written and tested as a "live product" with a realistic commit history. It is not production-ready and should not be deployed with real value without a third-party audit.


Architecture

                         ┌──────────────► WETH (Wrapped Ether)
                         │                 deposit / withdraw
   Router (periphery) ───┤
   add/remove liquidity  │
   multi-hop swaps       └──► PoolFactory ──createPool(A,B)──► LiquidityPool  (one per pair)
   ETH <-> token swaps         │                                 │
                               │ feeTo / feeToSetter             ├── ERC-20 LP token (BSW-LP)
                               │ (protocol-fee switch)           ├── reserves (packed) + TWAP accumulators
                               └─────────────────────────────────┴── swap / mint / burn
Contract Role Status
src/core/LiquidityPool.sol Constant-product pool: swap, mint/burn, TWAP, protocol fee implemented
src/core/PoolFactory.sol Deploys & registers pools, controls protocol-fee switch implemented
src/periphery/Router.sol Liquidity + multi-hop swaps + ETH swaps implemented
src/token/WETH.sol Minimal Wrapped Ether (deposit/withdraw) implemented
src/token/DEXToken.sol BSW token — ERC-20 + AccessControl, capped supply implemented
src/libraries/UQ112x112.sol Fixed-point math for TWAP accumulators implemented
src/interfaces/IWETH.sol WETH interface used by the Router implemented
src/mocks/MockERC20.sol ERC-20 mock test-only

LiquidityPool — key features

  • Constant-product curve (x · y = k)
  • 0.3% trading fee via the canonical Uniswap V2 formula:
    amountInWithFee = amountIn * 997
    amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee)
    
  • First mint: geometric mean — Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
  • MINIMUM_LIQUIDITY = 1000 burned to address(1) on first mint — protection against the first-LP inflation attack
  • Subsequent mints: proportional via Math.min(...)
  • TWAP price oracle: price0CumulativeLast / price1CumulativeLast accumulate time-weighted prices (UQ112.112 fixed point). Reserves and blockTimestampLast are packed into a single storage slot (uint112, uint112, uint32); the accumulator update is done in an unchecked block, matching Uniswap V2's intentional timestamp/price overflow behavior.
  • Optional protocol fee via _mintFee() and kLast: when factory.feeTo() is set, ~1/6 of the growth in sqrt(k) is minted to feeTo as LP tokens on the next mint/burn — the same mechanism as Uniswap V2's fee switch.
  • Inline post-swap invariant check inside swap():
    if (balance0Adjusted * balance1Adjusted < uint256(reserve0) * uint256(reserve1) * 1000 * 1000) {
        revert LiquidityPool__InvariantBroken();
    }
    The same fee-adjusted k-check Uniswap V2 uses.
  • Reserve overflow guard: _update reverts with LiquidityPool__Overflow if a balance exceeds type(uint112).max
  • ERC-20 LP token (BSW-LP) inheriting OpenZeppelin ERC-20
  • ReentrancyGuard on mint, burn, swap
  • SafeERC20 for all token transfers
  • Anti-griefing: swap reverts if to == token0, to == token1, or to == address(0)
  • Custom errors: LiquidityPool__ZeroAddress, LiquidityPool__CantBeZero, LiquidityPool__PoolIsVoid, LiquidityPool__AddressToken, LiquidityPool__InvariantBroken, LiquidityPool__Overflow
  • Events: LiquidityAdded, LiquidityRemoved, Swap

PoolFactory — key features

  • createPool(tokenA, tokenB) sorts the pair and deploys a new LiquidityPool
  • getPools[tokenA][tokenB] returns the pool (symmetric mapping) or address(0)
  • allPools array enumerates all created pools
  • Protocol-fee switch: feeTo / feeToSetter, changeable only by the current feeToSetter
  • Errors: PoolFactory__ZeroAddress, PoolFactory__AlreadyExist, PoolFactory__TokensAreEqual, PoolFactory__ForbiddenAddress
  • Events: PoolAdded, FeeToChanged, FeeToSetterChanged

Router — key features

  • Liquidity: addLiquidity / removeLiquidity with optimal-amount quoting, amountMin slippage bounds, and a deadline guard
  • Swaps: swapExactTokensForTokens with multi-hop routing through an arbitrary path
  • Native ETH: swapExactETHForTokens, swapExactTokensForETH, addLiquidityETH (refunds unused ETH via a Refund event), removeLiquidityETH — all wrapping/unwrapping through WETH
  • ensure(deadline) modifier on every state-changing entry point
  • receive() restricted to the WETH contract (Router__OnlyWETH)
  • Errors: Router__PoolNotFound, Router__ZeroAddress, Router__CantBeZero, Router__InsufficientAAmount, Router__InsufficientBAmount, Router__Expired, Router__InvalidPath, Router__OnlyWETH, Router__TransferFailed

DEXToken (BSW)

Utility/reward token, separate from the LP token:

  • ERC-20 + OpenZeppelin AccessControl
  • MINTER_ROLE gates minting; MAX_SUPPLY = 100,000,000 * 1e18 is enforced on every mint
  • Holder-initiated burn

Testing

forge test                                          # all tests (86 passing)
forge test -vvv                                     # verbose
forge test --match-contract LiquidityPoolFuzzTest   # fuzz only

86 tests passing, 0 failing across:

Suite Focus
test/unit/LiquidityPool.t.sol Minting, burning, swaps, reserves, invariant reverts
test/unit/PoolFactory.t.sol Pool creation, token sorting, fee-switch access control
test/unit/Router.t.sol add/remove liquidity, single- & multi-hop swaps, ETH paths, slippage & deadline reverts
test/unit/WETH.t.sol deposit, withdraw, receive, failed-transfer path
test/unit/DEXToken.t.sol mint role, max-supply cap, burn
test/fuzz/LiquidityPool.fuzz.t.sol testFuzz_swapInvariant — k-invariant holds across random swaps · testFuzz_cumulatives_linearInTime — TWAP accumulator grows linearly in time

The fuzz suite is the most interesting part: across many random trade sequences it verifies that the constant-product invariant is never violated, and that the TWAP accumulators advance proportionally to elapsed time.

CI

GitHub Actions (.github/workflows/test.yml) runs on every push / PR: forge fmt --checkforge build --sizesforge test -vvv.


Build & Run

git clone https://github.com/kiko21213/BetterSwap
cd BetterSwap
forge install
forge build
forge test

Build settings (foundry.toml): via_ir = true, optimizer_runs = 200.


Security considerations

Implemented Uniswap-V2-style protections:

  • ReentrancyGuard on all state-changing pool functions
  • SafeERC20 for every transfer
  • Inline fee-adjusted post-swap k-invariant check
  • MINIMUM_LIQUIDITY first-mint burn (anti first-LP attack)
  • Reserve overflow guard against the uint112 reserve bound
  • Anti-griefing checks on the swap recipient
  • Deadline + slippage bounds on all Router entry points
  • Role-gated protocol-fee switch and token minting

Known limitations — honest accounting

Useful for any reviewer before assuming production readiness:

  • Not professionally audited. Portfolio code; do not deploy with real value without a third-party audit.
  • Not deployed. No deployment/migration scripts yet (there is no script/ directory) — the project is exercised through tests only.
  • No fee-on-transfer / rebasing token support — pools assume standard ERC-20 transfer semantics.
  • No flash swapsswap() does not invoke a callback on the recipient, so V2-style flash swaps are not supported.
  • No V3 mechanics — concentrated liquidity is intentionally out of scope.
  • No cross-chain / bridge integration — intentionally out of scope.

License

MIT — SPDX identifiers are present in every source file. (A standalone LICENSE file has not been added to the repo root yet.)


Author

0xkiko — Solidity developer, security-focused

About

Uniswap-V2-style AMM in Solidity / Foundry -LiquidityPool with full swap and inline k-invariant check, PoolFactory, fuzz-verified constant-product invariant.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages