Skip to content

Repository files navigation

SwapGuard

SwapGuard is an educational Uniswap V2 integration built with Solidity, Foundry, React, TypeScript, viem, and wagmi. It demonstrates exact-input ERC-20 swaps with router quotations, bounded slippage, deadlines, direct and multi-hop paths, temporary allowances, mock tests, and a reproducible Arbitrum fork.

Open the live portfolio demo · Review the contract · Read the architecture

SwapGuard has not been audited. It is a portfolio project, not a recommendation to manage meaningful funds.

What this project proves

The repository shows the complete integration boundary between a wallet, an application contract, ERC-20 tokens, and an external DEX router. Reviewers can inspect validation and allowance decisions on-chain, realistic isolated tests, a real-protocol fork suite, executable local scripts, and a bigint-only browser flow.

Core concepts

  • A DEX exchanges assets through smart contracts. An AMM prices trades from pool reserves rather than an order book.
  • A liquidity pool holds the token pair; a router chooses and calls one or more pools.
  • An ERC-20 allowance authorizes transferFrom. The user approves SwapGuard; SwapGuard temporarily approves the router.
  • An exact-input swap fixes amountIn. amountOutMin is the least acceptable output after slippage.
  • Slippage is the allowed difference from a quote. The deadline prevents execution after a timestamp.
  • A path is the ordered token route. Two tokens are direct; three or more are multi-hop.
  • A router quote reflects current pool state. It can change before execution and is not an oracle.
  • A fork test executes against real deployed bytecode and historical state without spending real funds.

Architecture

flowchart LR
    User["User wallet"] -->|approve exact input| Guard["SwapGuard"]
    Web["React + wagmi client"] -->|quote and transact| Guard
    Guard -->|temporary exact allowance| Router["Uniswap V2-compatible router"]
    Router --> Pools["Liquidity pools"]
    Pools --> Router
    Router -->|output sent directly| User
Loading

SwapGuard never intentionally receives the output. It checks that its input-token balance returns to the pre-swap value and clears any router allowance.

Repository structure

src/SwapGuard.sol                         focused integration contract
src/interfaces/IUniswapV2Router02.sol    minimal external interface
test/unit/SwapGuard.t.sol                isolated unit and fuzz suite
test/mocks/                              behavioral token/router doubles
test/fork/SwapGuardArbitrumFork.t.sol    pinned real-router tests
script/                                  Foundry deployment and swap scripts
scripts/                                 local Anvil workflow
web/                                     Vite React client and utility tests
docs/                                    architecture, decisions, and review

Requirements

  • Foundry 1.5 or newer
  • Node.js 22 or newer and npm 10 or newer
  • An Arbitrum archive-capable RPC URL for fork tests/local demonstration
  • MetaMask or another injected wallet for the browser demo

Install and verify

git clone https://github.com/alsaecas/swapguard.git
cd swapguard
forge install foundry-rs/[email protected] --no-git
forge install OpenZeppelin/[email protected] --no-git
npm --prefix web ci
forge fmt --check
forge build
forge test --match-path 'test/unit/**'
forge coverage --match-path 'test/unit/**'
npm --prefix web run lint
npm --prefix web run test
npm --prefix web run build

To run fork tests:

cp .env.example .env
# Add ARBITRUM_RPC_URL to .env, then export it without committing the file.
set -a; source .env; set +a
forge test --match-path 'test/fork/**' -vvv

Environment

.env is ignored. .env.example contains no secrets.

Variable Purpose
VITE_APP_MODE Frontend mode: portfolio (default), local, or testnet
VITE_GITHUB_URL Public source link shown by the frontend
ARBITRUM_RPC_URL Archive RPC used at pinned block 250000000
PRIVATE_KEY Local Anvil account used by scripts; never use a real key
ROUTER_ADDRESS Validated router passed to deployment
SWAP_GUARD_ADDRESS Local deployment used by the execution script
TOKEN_IN_ADDRESS, TOKEN_OUT_ADDRESS Script token route
AMOUNT_IN, SLIPPAGE_BPS, DEADLINE_MINUTES Swap parameters in integer units
VITE_CHAIN_ID, VITE_RPC_URL Browser-visible local chain configuration
VITE_SWAP_GUARD_ADDRESS Browser-visible local deployment
VITE_TOKEN_IN_ADDRESS, VITE_TOKEN_OUT_ADDRESS Browser token configuration
VITE_BLOCK_EXPLORER_URL Optional explorer base URL for live transaction links

Anvil default keys are public. They must never be used on any real network.

Frontend modes

  • portfolio is the public Vercel experience. It needs no wallet, contract, or RPC configuration and uses a clearly labelled deterministic preview; it never submits a transaction.
  • local enables wallet and contract interaction only when every required public variable is complete and valid. It is intended for the reproducible Anvil fork below.
  • testnet uses the same strict live-mode checks, but is deliberately not configured in the hosted deployment because no router, liquid token pair, faucet path, and deployed SwapGuard instance have all been verified together.

The Vercel project builds from web/ with npm ci, npm run build, and the dist/ output. Production and preview deployments set VITE_APP_MODE=portfolio; no private key or server-side secret is used by the site.

Reproducible local demo

Use separate terminals. The first command stays in the foreground and exits with Ctrl-C.

set -a; source .env; set +a
make anvil-fork
export PRIVATE_KEY=<anvil-default-development-key>
make deploy-local
# Copy the logged address into SWAP_GUARD_ADDRESS and VITE_SWAP_GUARD_ADDRESS.
export LOCAL_ACCOUNT=<wallet-address>
./scripts/seed-local-account.sh
npm --prefix web run dev

Add http://127.0.0.1:8545, chain ID 31337, to MetaMask. Import only an Anvil default key, request a quote, approve the exact input, and execute the swap. A CLI alternative is make swap-local after exporting script variables.

Testing strategy

Mocks test failure modes and invariants without RPC access. Fork tests call the deployed Arbitrum router at a pinned block. Vitest checks browser-side parsing and previews; the contract remains authoritative at execution.

Behavior Type File
Slippage bounds and rounding Unit/fuzz test/unit/SwapGuard.t.sol
Direct and multi-hop routes Unit and fork quote unit/fork suites
Exact transfer and allowance cleanup Unit/fuzz/fork unit/fork suites
Expired deadline and impossible minimum Unit/fork unit/fork suites
Real router integration Fork test/fork/SwapGuardArbitrumFork.t.sol
Modes and environment validation Vitest web/src/config/env.test.ts
Parsing, formatting, deadline, route Vitest web/src/lib/utils.test.ts
Deterministic preview and quote invalidation Vitest web/src/lib/portfolio.test.ts

Security considerations

Slippage protects a minimum outcome but does not prevent front-running, sandwich attacks, or other MEV. A short deadline limits stale execution. The immutable router must be trusted; its response is structurally checked but its pricing is not an oracle. Exact temporary allowances reduce exposure. SafeERC20, non-reentrancy, strict paths, direct output delivery, and atomic reverts reduce common integration risks, but malicious or nonstandard tokens remain out of scope.

Limitations

This version supports standard ERC-20 token-to-token swaps only. It does not guarantee fee-on-transfer, rebasing, callback-capable, or otherwise unusual tokens; native ETH; wrapped-native convenience; oracle validation; price-impact display; DEX aggregation; private MEV protection; or production monitoring.

Future work could add EIP-2612, Permit2, Uniswap V3, aggregation, oracle checks, private submission, native ETH, and explicit fee-on-transfer support.

See SECURITY.md, docs/ARCHITECTURE.md, and DEMO_SCRIPT.md.

License

MIT

About

Secure Uniswap V2 swaps with slippage protection, React, and reproducible Arbitrum fork tests

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages