Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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