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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Test
run: npm test
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
lib/
node_modules/

# Secrets — never commit real wallets, keys, or env files
*.crypt
wallets/*
!wallets/template.json
!wallets/template.json
.env
.env.*
!.env.example

# Test / build artifacts
coverage/
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Changelog

All notable changes to this project are documented here. This project loosely
follows [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased] — 2026 modernization pass

A focused pass hardening the security surface, adding test coverage, and
trimming dead code, without changing the core execution design.

### Added

- **Test suite (vitest).** Unit tests for the pure execution logic — slippage
tolerance, gas normalization / ceiling / multiplier and their basis-point
integer-safety, DEX adapter selection, and the crypt round-trip (including
legacy-format decryption). Run with `npm test`.
- **CI.** GitHub Actions workflow running `build` + `test` on push and PRs.
- **`src/libs/execution.ts`.** The numeric trade-construction logic extracted
into pure, dependency-free functions so it can be tested in isolation.
- **`--exact-approval` option.** Approves only the current trade's spend amount
instead of an unlimited (max-uint) allowance. Default behaviour unchanged.
- **Base (L2) chain config.** `configs/chains/base.toml` with the official
Uniswap V2 deployment on Base, usable with the existing Primary adapter.

### Changed

- **Key derivation is now scrypt** (memory-hard, per-file random salt) instead
of an unsalted MD5. New wallet files carry a `GMBTv1` prefix; existing files
still decrypt via a legacy fallback, so no re-encryption is required.
- **Secrets stay off the command line.** The wallet password and private key
are prompted for (hidden) or read from `GAMBIT_PASSWORD` /
`GAMBIT_WALLET_KEY` rather than passed as CLI arguments (which leak into
shell history and the process listing). `setup-wallet`'s signature is now
`setup-wallet <walletName> <address>`.

### Fixed

- **Gas ceiling never bound.** The block-gas ceiling was computed as ~60× the
block gas limit rather than the intended 60%, so an over-estimate was never
clamped. `blockGasCeiling` now takes the documented percentage of the
normalized block gas limit.

### Removed

- Unused dependencies (`express`, `lowdb`, `node-notifier`, `log-update`).
- The unregistered `encrypt` / `decrypt` command handlers.
- Debug `console.log` / `console.dir` output on the buy/sell hot paths, a stray
target-token approval in `Primary.buyNative`, and commented-out scaffolding.
257 changes: 232 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,245 @@
![](https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/46e6b448-76a4-4f59-af76-185e8b320111/ddu2els-d7fd9241-0f1b-4392-b29c-b51686d918a9.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzQ2ZTZiNDQ4LTc2YTQtNGY1OS1hZjc2LTE4NWU4YjMyMDExMVwvZGR1MmVscy1kN2ZkOTI0MS0wZjFiLTQzOTItYjI5Yy1iNTE2ODZkOTE4YTkucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.LQF--3umAjkcAha3agv1kIcQYuUfk3ESLnVI6ppNIv4)

# Gambit
**LP Sniper**

### Install
**In terminal**
**A low-latency EVM execution engine for time-sensitive DEX trades, with a
pluggable multi-DEX adapter layer.**

Gambit constructs, prices, and submits swap transactions against
Uniswap-V2-style AMMs across multiple EVM chains, optimizing for the moment
liquidity appears. Under the hood it is a small study in the problems that make
on-chain execution hard: routing across DEXes whose router ABIs disagree,
choosing a gas limit under a moving block-gas ceiling, bounding slippage in
integer wei math, and keeping signing keys off disk in plaintext and off the
command line.

The original use case — sniping a liquidity pool the instant it opens — is what
forced those problems into the open. That use case is described below, but it is
the *problem domain*, not the point. The point is the execution infrastructure.

> **Portfolio note.** This repo is a sanitized version of a bot I ran a few
> years ago, brought up to date as a portfolio piece. It ships no wallets, no
> keys, and no private configuration — only public contract addresses. The
> [`CHANGELOG`](./CHANGELOG.md) records the 2026 modernization pass, and the
> [Design Observations](#design-observations-gaps--risks) section is a candid
> account of what I'd keep, what I fixed, and what I'd still change.

---

## What it demonstrates

- **Low-latency execution.** A tight pair-discovery loop, cached token metadata,
parallelized RPC reads, and a pending-nonce fast path so a buy can fire the
moment an operating pair with sufficient liquidity is found.
- **A multi-DEX adapter layer.** Different DEXes ship subtly different router
ABIs. Gambit hides those differences behind a uniform adapter interface so the
trade logic never branches on which DEX it's talking to (see below — this is
the centerpiece).
- **Gas optimization under adversarial conditions.** Estimated gas is clamped to
a percentage of the live block gas limit, then scaled by a safety multiplier —
all in integer basis-point math to avoid floating-point drift on-chain.
- **Safe transaction construction.** Slippage-bounded minimum-out, explicit gas
price / limit / nonce, and fee-on-transfer-safe swap selectors throughout.
- **Typed contract bindings.** Every contract call goes through TypeChain-
generated types (ERC-20, factory, pair, and per-DEX router variants), so a
wrong argument is a compile error, not a reverted transaction.
- **Encrypted key handling.** Wallet keys are stored AES-256-CBC encrypted under
a scrypt-derived key, and secrets are prompted for rather than passed on the
command line.

## The adapter pattern (the centerpiece)

The most valuable thing here is the abstraction that only gets written *after*
you've been burned by production DEX quirks. Every DEX in this space is "a
Uniswap V2 fork," right up until it isn't:

- **Camelot** (Arbitrum) adds a **referrer address** argument in the middle of
its swap selectors. Same function name, different arity.
- **Glacier** (Avalanche, a Solidly/Velodrome fork) threads a **`stable` boolean
flag** through pair lookup — `getPair(a, b, stable)` — because it maintains
separate stable and volatile pools for the same token pair.
- Everything else — Pancake, SushiSwap, Uniswap V2, and friends — speaks the
vanilla V2 router interface.

Rather than scatter `if (dex === 'camelot')` across the trade path, Gambit
resolves a **pair adapter** and a **factory adapter** once, and the buy/sell
logic calls a uniform interface (`buy`, `buyNative`, `sell`, `sellNative`,
`getPair`). Adding a new DEX with its own quirk is a new file in
[`src/libs/dex/`](./src/libs/dex/) plus its TypeChain-generated router — not a
change to the execution core.

```ts
// src/libs/pair.ts — selection happens once, per trade
const pairAdapters = { camelot: Camelot };

export const getPairAdapter = (dexName: string, pair: Pair) => {
if (!Object.keys(pairAdapters).includes(dexName)) {
return new Primary(pair); // vanilla V2 default
}

const Adapter = pairAdapters[dexName as keyof typeof pairAdapters];

return new Adapter(pair);
};
```

## Architecture

Layered, with each layer depending only on the one below it. Selection of the
chain, DEX, and adapter is data-driven from per-chain TOML config.

```mermaid
flowchart TD
CLI["CLI (commander)<br/>gambit.ts · snipe / setup-wallet"]
PROMPT["prompt service<br/>hidden secret input · env fallback"]
CRYPT["crypt service<br/>scrypt KDF · AES-256-CBC"]
SNIPER["Sniper<br/>wallet load · pair discovery · best-liquidity pick"]
CHAIN["Chain<br/>RPC providers · token cache · TOML config"]
DEX["Dex<br/>router/factory resolution · adapter selection"]
PAIR["Pair<br/>buy / sell · approvals"]
EXEC["execution.ts (pure)<br/>slippage · gas ceiling · multiplier"]
ADAPTERS["DEX adapters<br/>Primary · Camelot · Glacier"]
TYPECHAIN["TypeChain bindings<br/>Erc20 · Router* · Factory* · Pair"]

CLI --> PROMPT
CLI --> CRYPT
CLI --> SNIPER
SNIPER --> CHAIN
CHAIN --> DEX
DEX --> PAIR
PAIR --> EXEC
PAIR --> ADAPTERS
DEX --> ADAPTERS
ADAPTERS --> TYPECHAIN
CHAIN --> TYPECHAIN
```

| Layer | Responsibility |
| --- | --- |
| **CLI** ([`gambit.ts`](./src/gambit.ts)) | Argument parsing; resolves secrets via prompt/env before doing anything. |
| **Sniper** ([`libs/sniper.ts`](./src/libs/sniper.ts)) | Loads the wallet, discovers the operating pair, and picks the most-liquid source token. |
| **Chain** ([`libs/chain.ts`](./src/libs/chain.ts)) | RPC providers, wallet connection, token metadata caching, per-chain config. |
| **Dex** ([`libs/dex.ts`](./src/libs/dex.ts)) | Resolves router + factory contracts and selects the right adapters. |
| **Pair** ([`libs/pair.ts`](./src/libs/pair.ts)) | Prices, builds, and submits buys/sells; manages approvals. |
| **execution.ts** ([`libs/execution.ts`](./src/libs/execution.ts)) | Pure slippage + gas math. Fully unit-tested. |
| **Adapters** ([`libs/dex/`](./src/libs/dex/)) | Per-DEX router/factory quirk handling behind a uniform interface. |
| **TypeChain** ([`typechain/`](./src/typechain/)) | Generated, typed contract bindings. |

## Install

Requires Node 20+.

```bash
npm install
npm run build
npm test
```

## Usage

Run `./gambit.js` to see available commands. Secrets are never taken as CLI
arguments — they're prompted for (hidden) or read from the environment.

### `setup-wallet`

Encrypts a wallet address + private key into `wallets/<name>.crypt` (scrypt +
AES-256-CBC). The private key and password are prompted for:

```bash
./gambit.js setup-wallet mainWallet 0xYourAddress
# prompts (hidden): private key, password, confirm password

# Non-interactive (e.g. CI/automation):
GAMBIT_WALLET_KEY=... GAMBIT_PASSWORD=... ./gambit.js setup-wallet mainWallet 0xYourAddress
```

### `snipe`

Watches for an operating pair for the target token and, once found, either runs
an interactive buy/sell shell or executes an automatic spend. The wallet
password is prompted for when the wallet is encrypted:

```bash
./gambit.js snipe mainWallet arb camelot 0xTargetToken
# add --totalSpend 0.01 for automatic mode
# add --exact-approval to approve only the spend amount (see Risks)
# set GAMBIT_PASSWORD to run without the prompt
```

See `./gambit.js snipe --help` for the full option list.

## Chains & DEXes

Chain and DEX configuration lives in [`configs/`](./configs/) as per-chain TOML,
alongside the relevant ABIs. Adding a chain is a config file; adding a
standard-V2 DEX is a `[[dexes]]` entry. Currently shipped:

| Chain | DEXes | Notes |
| --- | --- | --- |
| **Base** | uniswap | Official Uniswap V2 deployment; vanilla adapter. |
| **Arbitrum** | camelot, sushi, lizard, alienfi | Camelot uses the referrer-arg adapter. |
| **Avalanche** | glacier, pangolin | Glacier uses the stable-flag factory adapter. |
| **BSC** | pancake, apebsc | Vanilla adapters. |

## Testing

1. `yarn`
2. `yarn build`
```bash
npm test # vitest, one-shot
npm run test:watch
```

### Usage
Run `./gambit.js` to view the available commands and formatting
The pure execution logic (slippage, gas ceiling/multiplier, basis-point
integer-safety), adapter selection, and the crypt round-trip (including legacy
decryption) are unit-tested. CI runs `build` + `test` on every push and PR.

Currently there are 2 commands available:
## Design observations, gaps & risks

#### setup-wallet
Senior engineering is as much about knowing a system's limits as building it.
This is a candid account — including the things this pass **fixed**. The
[`CHANGELOG`](./CHANGELOG.md) and commit history link each change to its diff.

Encrypts your wallet file containing your wallet address and private key. You can see the format and example usage by running `./gambit.js setup-wallet --help`
**Fixed in the 2026 pass**

#### snipe
- **KDF was MD5.** Wallet keys were encrypted under an *unsalted MD5* of the
password — fast to brute-force. Replaced with memory-hard **scrypt** + a
per-file salt, with a backward-compatible decrypt path
([`src/services/crypt.ts`](./src/services/crypt.ts)).
- **Secrets on argv.** Passwords and private keys were positional CLI arguments,
which leak into shell history and `ps` output. They're now **prompted for
(hidden)** or read from the environment
([`src/services/prompt.ts`](./src/services/prompt.ts)).
- **Gas ceiling never bound.** The block-gas ceiling was computed as ~60× the
block limit rather than the intended 60%, so an over-estimate was never
actually clamped. Fixed and unit-tested
([`src/libs/execution.ts`](./src/libs/execution.ts)).

Runs the actual sniper with the given arguments, you can see the format and example usage by running `./gambit.js snipe --help`
**Known tradeoffs & remaining gaps**

### Chains/Dexes
Chain and dex configurations are stored in the `/configs` directory alongside any relevant ABI's
- **Unlimited approvals by default.** The router is granted a max-uint allowance
so one approval covers every future trade — the right latency/gas tradeoff for
sniping, but it leaves the balance exposed to a compromised router. The
`--exact-approval` flag opts into per-trade allowances instead.
- **Public-mempool submission.** Trades go to the public mempool and are
therefore sandwichable. Slippage bounds limit the damage but don't prevent it;
private orderflow would (see below).
- **`ethers` v5.** Pinned to v5 for the original TypeChain bindings; v6 is the
current line.
- **Coverage is targeted, not total.** Tests cover the pure logic and key
handling. The RPC-bound orchestration (pair discovery, submission) is exercised
manually, not mocked end-to-end.

Currently the supported chains & dexes are:
## What I'd build differently in 2026

#### BSC
- pancake
- apebsc
The core execution ideas hold up; the environment around them has moved.

#### ARB
- sushi_arb
- camelot
- lizard
- alienfi (untested)
- **Private orderflow instead of the public mempool.** Submit via a bundle relay
(Flashbots-style) or an L2 sequencer's private endpoint to defeat sandwiching —
the single biggest correctness/PnL win.
- **Account abstraction (ERC-4337).** Session keys and a smart account would
remove raw-private-key handling from the hot path and enable atomic
approve-and-swap via bundled user operations.
- **L2-native fee modeling.** On Base/Arbitrum the dominant cost is L1 data, not
L2 gas. A 2026 build would model calldata cost explicitly rather than reusing
an L1-style gas-limit heuristic.
- **`ethers` v6 + viem.** Modern typings, smaller bundles, and first-class
multicall for the read-heavy discovery loop.
- **Simulation before submission.** `eth_call` / state-override simulation of the
exact swap to catch honeypots and fee-on-transfer surprises before spending gas.
Loading
Loading