Skip to content
140 changes: 140 additions & 0 deletions CONTRACTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# CONTRACTS.md — Deployed Contract Addresses & Roles

## Base Mainnet (Chain ID: 8453)

### Panorama Infrastructure

| Contract | Address | Role |
|---|---|---|
| PanoramaExecutorV2 | `0x7528861E7DD09dc9B1e5149542e897d984Ceda7f` | Single entry point — routes `execute()` calls to per-user BeaconProxy adapters |
| AerodromeAdapterV2 | `0x187e499afB2DE75836800ad19147e0cFcd2Dc715` | Beacon implementation for Aerodrome (swap, LP, stake/unstake, claim) |
| DCAVault | `0x155eC4256cC6f11f3d4C21Af28a2a1CC31f730d1` | Dollar-cost averaging vault (uses IPanoramaExecutor interface) |

### Aerodrome Finance (DEX + Gauges)

| Contract | Address | Role |
|---|---|---|
| Router | `0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43` | AMM router for swaps and liquidity |
| Factory | `0x420DD381b31aEf6683db6B902084cB0FFECe40Da` | Pool factory (immutable pool addresses) |
| Voter | `0x16613524e02ad97eDfeF371bC883F2F5d6C480A5` | Gauge registry — maps pool -> gauge |

### Tokens

| Token | Address | Decimals |
|---|---|---|
| WETH | `0x4200000000000000000000000000000000000006` | 18 |
| USDC | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | 6 |
| USDbC | `0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA` | 6 |
| AERO | `0x940181a94A35A4569E4529A3CDfB74e38FD98631` | 18 |
| cbBTC | `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` | 8 |
| wstETH | `0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452` | 18 |
| cbETH | `0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22` | 18 |
| DAI | `0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb` | 18 |

---

## Avalanche C-Chain (Chain ID: 43114)

### Panorama Infrastructure

| Contract | Address | Role |
|---|---|---|
| PanoramaExecutorV2 | `0xc35059D1BC395Ff0F6fDcEA1b7F365E3aa7C1D12` | Single entry point — same pattern as Base |

### Trader Joe V1 (DEX)

| Contract | Address | Role |
|---|---|---|
| Router | `0x60aE616a2155Ee3d9A68541Ba4544862310933d4` | AMM router for swaps |

Protocol ID: `keccak256("traderjoe")`

### Benqi Finance (Lending)

| Contract | Address | Role |
|---|---|---|
| Comptroller | `0x486Af39519B4Dc9a7fCcd318217352830E8AD9b4` | Lending market controller |
| qiAVAX | `0x5C0401e81Bc07Ca70fAD469b451682c0d747Ef1c` | AVAX lending market (qToken) |
| qiUSDC.e | `0xBEb5d47A3f720Ec0a390d04b4d41ED7d9688bC7F` | USDC.e lending market |
| qiUSDT | `0xc9e5999b8e75C3fEB117F6f73E664b9f3C8ca65C` | USDT lending market |
| qiETH | `0x334AD834Cd4481BB02d09615E7c11a00579A7909` | WETH.e lending market |

Protocol ID: `keccak256("benqi")`

### sAVAX (Liquid Staking)

| Contract | Address | Role |
|---|---|---|
| StakedAvax (sAVAX) | `0x2b2C81e08f1Af8835a78Bb2A90AE924ACE0eA4bE` | AVAX liquid staking derivative |

Protocol ID: `keccak256("savax")`

### Tokens

| Token | Address | Decimals |
|---|---|---|
| WAVAX | `0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7` | 18 |
| USDC | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | 6 |
| USDCe | `0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664` | 6 |
| USDT | `0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7` | 6 |
| USDTe | `0xc7198437980c041c805A1EDcbA50c1Ce5db95118` | 6 |
| WETHe | `0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB` | 18 |
| sAVAX | `0x2b2C81e08f1Af8835a78Bb2A90AE924ACE0eA4bE` | 18 |

---

## Architecture

### BeaconProxy Pattern (V2)

```
User -> PanoramaExecutorV2.execute(protocolId, action, transfers, deadline, data)
|
+-- looks up UpgradeableBeacon for protocolId
+-- creates or retrieves user's BeaconProxy
+-- pulls tokens from user to proxy (transfers[])
+-- calls proxy.call(action ++ data) -- blind dispatch
|
+-- BeaconProxy delegates to Adapter implementation
```

- `beacon.upgradeTo(newImpl)` upgrades ALL users at once
- Adapters use `Initializable` + `__gap[50]` for storage stability
- Executor never contains action-specific logic

### Protocol IDs

Protocol IDs are `bytes32 = keccak256(protocolName)`. Backend uses `encodeProtocolId("name")` from `utils/encoding.ts`.

| Protocol | Name String | Chain |
|---|---|---|
| Aerodrome | `"aerodrome"` | Base |
| Trader Joe | `"traderjoe"` | Avalanche |
| Benqi | `"benqi"` | Avalanche |
| sAVAX | `"savax"` | Avalanche |

### Adapter Conventions

All V2 adapters share:
- `initializeFull(address _executor, bytes calldata _initArgs) external initializer`
- `onlyExecutor` modifier (reverts with `OnlyExecutor()` custom error)
- `receive() external payable {}`
- `uint256[50] private __gap` for upgrade safety
- Custom errors (no `require` strings)

Known differences (deployed, cannot change):
- **AerodromeAdapterV2**: uses `SafeTransferLib` + double `safeApprove(0); safeApprove(amt)`
- **Avax adapters**: use OpenZeppelin `SafeERC20` + `forceApprove()`
- **BenqiLendAdapter**: has parameterized error `BenqiError(uint256)` for Comptroller error codes

### Environment Variables

```bash
# Base
BASE_RPC_URLS=https://base.llamarpc.com,https://mainnet.base.org,https://base.drpc.org
EXECUTOR_ADDRESS=0x7528861E7DD09dc9B1e5149542e897d984Ceda7f

# Avalanche
AVAX_RPC_URLS=https://api.avax.network/ext/bc/C/rpc,https://avalanche.drpc.org,https://avax.meowrpc.com
AVAX_EXECUTOR_ADDRESS=0xc35059D1BC395Ff0F6fDcEA1b7F365E3aa7C1D12
```
169 changes: 168 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ forge test -vv --no-match-path "test/fork/*"
# Fork tests (requires RPC)
BASE_RPC_URL=https://mainnet.base.org forge test --match-path "test/fork/*" -vvv

# Backend (Vitest) — 138 tests
# Backend (Vitest) — 187 tests
cd backend && npm test
```

Expand Down Expand Up @@ -430,3 +430,170 @@ No changes needed to the executor or BundleBuilder core.
| Testing | Foundry (Solidity), Vitest (TypeScript) |
| Chains | Base (8453), Avalanche C-Chain (43114) |
| Protocols | Aerodrome Finance, Trader Joe, Benqi Finance, BENQI sAVAX |

---

## Backend Infrastructure

### Caching (`shared/cache.ts`)

TTL-based cache with stale fallback for graceful degradation:

```typescript
const myCache = createCache<MyType>();
setCache(myCache, key, value, 30_000); // 30s TTL
const fresh = getCached(myCache, key); // null if expired
const stale = getStale(myCache, key); // { value, stale: true } if expired but exists
```

**Cache tiers across the backend:**

| Data | TTL | Rationale |
|---|---|---|
| Pool addresses | 10 min | Immutable on-chain |
| Gauge addresses | 5 min | Can change via governance |
| Token metadata (symbol/decimals) | 1 hour | Never changes |
| Gauge reward rate | 60s | Updates per epoch |
| Portfolio per user | 30s | Balances change frequently |
| DexScreener metrics | 30s | External API |
| Wallet balances | 90s | Moderate refresh |

All caches use **in-flight dedup** (`Map<string, Promise<T>>`) to prevent thundering herd on concurrent requests for the same key.

### RPC Provider Failover (`providers/chain.provider.ts`)

Multiple free RPC endpoints per chain with automatic failover:

```
Primary RPC (3.5s timeout)
↓ fail
Parallel race across fallback RPCs (3.5s each)
↓ fail
Mark primary as "sick" (30s cooldown), retry next request on fallback
```

**Default RPCs:**
- **Base**: LlamaRPC, Base official, dRPC
- **Avalanche**: Avalanche official, dRPC, MeowRPC

Configured via `BASE_RPC_URLS` / `AVAX_RPC_URLS` (comma-separated). Health tracking with 30s recovery window.

### Structured Logging (`shared/logger.ts`)

Zero-dependency structured logger with per-request trace IDs:

```typescript
logger.info({ protocol: "aerodrome", pool: "WETH/USDC", durationMs: 45 }, "Quote obtained");
// → {"level":"info","traceId":"abc-123","protocol":"aerodrome","pool":"WETH/USDC","durationMs":45,"msg":"Quote obtained","ts":"2026-03-30T..."}
```

- **`AsyncLocalStorage`** propagates `traceId` across async call chains
- **JSON** output in production, **colored text** in development
- **Tracing middleware** (`middleware/tracing.ts`) auto-generates UUID per request and logs on response finish

### Rate Limiting (`middleware/rateLimiter.ts`)

Three-tier sliding-window rate limiter:

| Tier | Scope | Window | Max |
|---|---|---|---|
| IP | All endpoints | 60s | 60 req |
| Wallet | Per wallet address | 60s | 30 req |
| Prepare | `prepare-*` endpoints | 10s | 10 req |

Cascading check: IP → Wallet → Prepare. Expired entries cleaned every 5 minutes.

### Stale Fallback Pattern

On data fetch failure, the backend returns the last known good value instead of erroring:

```
Fresh fetch succeeds → cache + return
Fresh fetch fails → check stale cache
├── stale exists → return { ...data, stale: true }
└── no stale → throw error
```

Applied to: portfolio, protocol info, DexScreener metrics, wallet balances.

### Error Codes (`shared/errorCodes.ts`)

Standardized error responses via `AppError`:

| Category | Codes | HTTP |
|---|---|---|
| Validation | `INVALID_ADDRESS`, `INVALID_AMOUNT`, `MISSING_FIELD`, `INVALID_SLIPPAGE` | 400 |
| Not Found | `POOL_NOT_FOUND`, `GAUGE_NOT_FOUND`, `ORDER_NOT_FOUND` | 404 |
| Client | `INSUFFICIENT_BALANCE`, `NO_LIQUIDITY`, `NO_LP_POSITION`, `NO_REWARDS` | 400 |
| Auth | `INVALID_SIGNATURE`, `AUTH_EXPIRED` | 401 |
| Rate Limit | `RATE_LIMIT_EXCEEDED` | 429 |
| Server | `RPC_ERROR`, `PROVIDER_ERROR`, `INTERNAL_ERROR` | 500/502 |

### Cross-Chain Routing (Interface Only)

Domain ports for future bridge integration:

- **`domain/ports/RoutingPort.ts`** — aggregator: `getRoutes()`, `executeRoute()`, `getRouteStatus()`
- **`domain/ports/CrossChainMessagingPort.ts`** — per-protocol adapter (Wormhole, CCIP, LayerZero, LI.FI)
- **`types/cross-chain.ts`** — shared types: `CrossChainRoute`, `CrossChainFee`, `MessageStatus`, etc.

No implementation yet — interfaces ready for LI.FI or equivalent.

### Middleware Stack

Request pipeline (in order):

```
tracing → CORS → rateLimiter → serializeByUser → validation → executionTimeout → handler → errorHandler
```

| Middleware | File | Purpose |
|---|---|---|
| `tracingMiddleware` | `middleware/tracing.ts` | UUID traceId per request |
| `rateLimiter` | `middleware/rateLimiter.ts` | 3-tier rate limiting |
| `serializeByUser` | `middleware/serialize-by-user.ts` | Queue concurrent requests per wallet |
| `validation` | `middleware/validation.ts` | Address, amount, tx hash, slippage checks |
| `executionTimeout` | `middleware/execution-timeout.ts` | 15s hard timeout per request |
| `errorHandler` | `middleware/errorHandler.ts` | AppError → structured JSON response |

---

## Test Coverage

```bash
cd backend && npm test
# 187 tests across 10 test suites
```

| Suite | Tests | Coverage |
|---|---|---|
| `e2e/demo-flow` | 49 | Full H5 demo flow (12 iterations), fallback messaging, bundle invariants |
| `integration/routes` | 16 | Swap + staking + claim + exit bundles via usecases |
| `modules/swap/get-quote` | 16 | Auto pool selection, slippage, exchange rate |
| `modules/swap/prepare-swap` | 11 | Approve logic, ETH handling, metadata |
| `modules/liquid-staking/prepare-enter` | 10 | Balance capping, liquidity quote, 5-step bundle |
| `modules/liquid-staking/prepare-exit` | 12 | Partial/full exit, unstake + removeLiquidity |
| `modules/liquid-staking/prepare-claim` | 8 | Reward check, single-step bundle |
| `shared/bundle-builder` | 25 | Selectors, approve logic, encode/decode |
| `shared/aerodrome-add-liquidity` | 11 | Allowance checks, slippage, stake amount |
| `shared/services/aerodrome.service` | 29 | Caching, in-flight dedup, retry, timeout |

### E2E Demo Flow Test (`__tests__/e2e/demo-flow.test.ts`)

Simulates the canonical H5 user journey **12 times** for determinism:

1. Quote swap (WETH → USDC, auto pool selection)
2. Prepare swap bundle (approve + execute)
3. Check portfolio (empty)
4. Enter staking (addLiquidity + stake)
5. Check portfolio (has position)
6. Claim rewards
7. Exit position (unstake + removeLiquidity)
8. Check portfolio (empty again)

Plus targeted tests for every common failure mode:
- **RPC timeout** → fallback to safe defaults (assume 0 allowance, skip balance check)
- **Insufficient balance** → `INSUFFICIENT_BALANCE` with have/need amounts
- **Pool not found** → `POOL_NOT_FOUND` with pool ID in message
- **No liquidity** → `NO_LIQUIDITY` for both auto-quote and enter
- **No position / No rewards** → `NO_LP_POSITION` / `NO_REWARDS`
30 changes: 26 additions & 4 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,36 @@ PORT=3010
NODE_ENV=development # development | demo | production

# ── Base Chain RPC ───────────────────────────────────────────────
# Comma-separated, priority order. First = primary, rest = failover.
# The provider tries primary with 3.5s timeout, then races all fallbacks
# in parallel. Sick RPCs (2+ consecutive failures) are skipped for 30s.
#
# Recommended free RPCs (no API key required):
# https://base.llamarpc.com — LlamaNodes, generous limits
# https://mainnet.base.org — Coinbase official, moderate limits
# https://base.drpc.org — dRPC free tier
# https://base.meowrpc.com — MeowRPC free tier
#
# Single RPC (backward-compatible):
BASE_RPC_URL=https://mainnet.base.org
# Multiple RPCs with failover (comma-separated, priority order):
# BASE_RPC_URLS=https://your-alchemy-base.com,https://base.llamarpc.com,https://mainnet.base.org
# BASE_RPC_URL=https://mainnet.base.org
#
# Multiple RPCs with failover:
BASE_RPC_URLS=https://base.llamarpc.com,https://mainnet.base.org,https://base.drpc.org

# ── Avalanche Chain RPC ──────────────────────────────────────────
# Same failover logic as Base. Comma-separated, priority order.
#
# Recommended free RPCs (no API key required):
# https://api.avax.network/ext/bc/C/rpc — Official, low rate limits
# https://avalanche.drpc.org — dRPC free tier
# https://avax.meowrpc.com — MeowRPC free tier
# https://rpc.ankr.com/avalanche — Ankr public
#
# Single RPC (backward-compatible):
# AVAX_RPC_URL=https://api.avax.network/ext/bc/C/rpc
# AVAX_RPC_URLS=https://your-alchemy-avax.com,https://api.avax.network/ext/bc/C/rpc,https://avalanche.drpc.org
#
# Multiple RPCs with failover:
AVAX_RPC_URLS=https://api.avax.network/ext/bc/C/rpc,https://avalanche.drpc.org,https://avax.meowrpc.com

# ── Deployed Contract Addresses (Base Mainnet) ───────────────────
EXECUTOR_ADDRESS=0x82b000512A19f7B762A23033aEA5AE00aBD0D2bC
Expand Down
Loading
Loading