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.
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.
- 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.amountOutMinis 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.
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
SwapGuard never intentionally receives the output. It checks that its input-token balance returns to the pre-swap value and clears any router allowance.
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
- 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
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 buildTo 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.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.
portfoliois the public Vercel experience. It needs no wallet, contract, or RPC configuration and uses a clearly labelled deterministic preview; it never submits a transaction.localenables wallet and contract interaction only when every required public variable is complete and valid. It is intended for the reproducible Anvil fork below.testnetuses 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.
Use separate terminals. The first command stays in the foreground and exits with Ctrl-C.
set -a; source .env; set +a
make anvil-forkexport 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 devAdd 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.
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 |
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.
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.
MIT