Skip to content

Commit 73b5d63

Browse files
committed
fix: make local bot production-like by default
- Change default spreads from 50 bps to 3 bps to match production risk profile - Set TTL to 120 seconds and refresh threshold to 10 bps instead of hardcoded local values (1800 seconds, 0 bps) to exercise real signing and nonce rotation behavior - Reduce feed staleness from 1 day to 900 seconds for realistic feed freshness handling - Set funding headroom to 1x (one book of inventory) instead of 100x; add logic to burn excess balance when headroom is 1 - Adjust oracle refresh interval to 300 seconds to stay within feed staleness window - Add `nonNegativeIntEnv` helper for zero-or-positive integer environment variables - Add `burn` function to ERC20 ABI for inventory normalization - Update documentation to clarify that production-like defaults are intentional and should only be overridden with `STITCH_LOCAL_*` env vars for explicit local testing - Log production-like configuration values on startup for visibility
1 parent 2eb77b3 commit 73b5d63

5 files changed

Lines changed: 56 additions & 31 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
acde1ca97884bc5297e6cbf559d2a8837701c723
1+
0a776a6e2a6678f5558fd2f3447e859411fdefad

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.40
1+
0.1.41

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.40"
3+
version = "0.1.41"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; signs UniswapX limit orders and closes settlement auctions."
66
license = "AGPL-3.0-or-later"

scripts/run-local.mjs

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
//
1010
// node packages/stitch-bot/scripts/run-local.mjs
1111
//
12-
// Env: STITCH_PRIVATE_KEY, FEED_URL (base), INDEXER_URL, BLOCKCHAIN_RPC_URL,
13-
// OFFSET_BPS / SELL_OFFSET_BPS (spreads), TOTAL_ORDER_SIZE_USD (per-side
14-
// notional), MIN_ORDER_SIZE_USD (smallest ladder slice).
12+
// Env: STITCH_PRIVATE_KEY, FEED_URL (base), INDEXER_URL, BLOCKCHAIN_RPC_URL.
13+
// Production-like defaults are intentional: short TTL, real refresh threshold,
14+
// one book of inventory, and max liquidity. Override STITCH_LOCAL_* only when
15+
// you deliberately want local convenience over production parity.
1516
import { spawn } from 'node:child_process'
1617
import { readFileSync, writeFileSync } from 'node:fs'
1718
import { fileURLToPath } from 'node:url'
@@ -27,8 +28,10 @@ const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
2728
const KEY =
2829
process.env.STITCH_PRIVATE_KEY ||
2930
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
30-
const BUY_OFFSET_BPS = Number(process.env.OFFSET_BPS || 50)
31-
const SELL_OFFSET_BPS = Number(process.env.SELL_OFFSET_BPS || 50)
31+
const BUY_OFFSET_BPS = Number(process.env.OFFSET_BPS || 3)
32+
const SELL_OFFSET_BPS = Number(
33+
process.env.SELL_OFFSET_BPS || process.env.OFFSET_BPS || 3
34+
)
3235
const TOTAL_ORDER_SIZE_USD = Number(
3336
process.env.TOTAL_ORDER_SIZE_USD || process.env.ORDER_SIZE_USD || 2000
3437
) // each side quotes roughly this many USD
@@ -38,14 +41,23 @@ const positiveIntEnv = (name, fallback) => {
3841
const value = Number.parseInt(process.env[name] || '', 10)
3942
return Number.isFinite(value) && value > 0 ? value : fallback
4043
}
44+
const nonNegativeIntEnv = (name, fallback) => {
45+
const value = Number.parseInt(process.env[name] || '', 10)
46+
return Number.isFinite(value) && value >= 0 ? value : fallback
47+
}
4148
const FEED_WAIT_SECONDS = positiveIntEnv('FEED_WAIT_SECONDS', 600)
4249
const FEED_PROBE_TIMEOUT_MS = positiveIntEnv('FEED_PROBE_TIMEOUT_MS', 8000)
43-
// The local feed is a static seeded rate (the oracle is flat — see
44-
// refresh_threshold_bps below) and nothing refreshes its observedAt while the
45-
// stack runs, so a tight staleness window makes the bot skip every pool ~1
46-
// minute after the last seed. Default to a day so a local stack quotes all day;
47-
// override with FEED_STALENESS_SECS to exercise real staleness handling.
48-
const FEED_STALENESS_SECS = positiveIntEnv('FEED_STALENESS_SECS', 86400)
50+
const FEED_STALENESS_SECS = positiveIntEnv('FEED_STALENESS_SECS', 900)
51+
const ORACLE_REFRESH_SECONDS = positiveIntEnv('ORACLE_REFRESH_SECONDS', 300)
52+
const STITCH_LOCAL_TTL_SECS = positiveIntEnv('STITCH_LOCAL_TTL_SECS', 120)
53+
const STITCH_LOCAL_REFRESH_THRESHOLD_BPS = nonNegativeIntEnv(
54+
'STITCH_LOCAL_REFRESH_THRESHOLD_BPS',
55+
nonNegativeIntEnv('REFRESH_THRESHOLD_BPS', 10)
56+
)
57+
const STITCH_LOCAL_FUNDING_HEADROOM = positiveIntEnv(
58+
'STITCH_LOCAL_FUNDING_HEADROOM',
59+
1
60+
)
4961

5062
const addrs = JSON.parse(
5163
readFileSync(`${CRATE}../constants/src/addresses.localhost.json`)
@@ -87,6 +99,7 @@ const erc20 = parseAbi([
8799
'function balanceOf(address) view returns (uint256)',
88100
'function allowance(address,address) view returns (uint256)',
89101
'function mint(address,uint256)',
102+
'function burn(address,uint256)',
90103
'function approve(address,uint256) returns (bool)',
91104
])
92105
const oracleAbi = parseAbi(['function buyRate() view returns (uint256)'])
@@ -260,7 +273,7 @@ const account = privateKeyToAccount(KEY)
260273
const wallet = createWalletClient({ account, chain, transport: http(RPC) })
261274
const MAX = (1n << 256n) - 1n
262275
for (const [token, amt] of Object.entries(need)) {
263-
const want = amt * 100n // headroom for many re-quotes
276+
const want = amt * BigInt(STITCH_LOCAL_FUNDING_HEADROOM)
264277
const [bal, allow] = await Promise.all([
265278
pub.readContract({
266279
address: token,
@@ -283,6 +296,23 @@ for (const [token, amt] of Object.entries(need)) {
283296
args: [account.address, want],
284297
})
285298
await pub.waitForTransactionReceipt({ hash })
299+
} else if (bal > want && STITCH_LOCAL_FUNDING_HEADROOM === 1) {
300+
try {
301+
const hash = await wallet.writeContract({
302+
address: token,
303+
abi: erc20,
304+
functionName: 'burn',
305+
args: [account.address, bal - want],
306+
})
307+
await pub.waitForTransactionReceipt({ hash })
308+
} catch {
309+
console.warn(
310+
` ${token} balance is above the production-like target and could ` +
311+
`not be burned down; "max" will quote the full local balance. Run ` +
312+
`yarn dev:reset or use a fresh STITCH_PRIVATE_KEY to test one-book ` +
313+
`funding exactly.`
314+
)
315+
}
286316
}
287317
if (allow < want) {
288318
const hash = await wallet.writeContract({
@@ -323,18 +353,8 @@ sell_offset_bps = ${SELL_OFFSET_BPS}
323353
sell_total_liquidity_collateral = "max"
324354
sell_min_slice_debt = "${p.minAskDebtAtomic}"
325355
sell_max_orders = ${MAX_LADDER_ORDERS}
326-
# The bot signs deadlines off its wall clock, but the local Hardhat chain's
327-
# block.timestamp can run minutes ahead of wall time (tests bump EVM time and
328-
# it can't go back). A short TTL then makes every order expire on-chain before
329-
# it's fillable — executeBatch reverts with Permit2 SignatureExpired. 30 min
330-
# absorbs realistic local drift. (Prod chains track wall time, so TTL there is
331-
# the bot's own short value.)
332-
ttl_secs = 1800
333-
# 0 → re-sign with a fresh Permit2 nonce every tick. The local oracle is flat,
334-
# so a price-gated bot would never rotate the nonce, and a filled order would
335-
# linger in the book and revert the next fill with InvalidNonce. (Production
336-
# uses a real threshold — the live feed moves, so nonces rotate on their own.)
337-
refresh_threshold_bps = 0
356+
ttl_secs = ${STITCH_LOCAL_TTL_SECS}
357+
refresh_threshold_bps = ${STITCH_LOCAL_REFRESH_THRESHOLD_BPS}
338358
`
339359
}
340360

@@ -346,6 +366,12 @@ for (const p of pools) {
346366
` ${p.key}: bid ${TOTAL_ORDER_SIZE_USD} stable / ask ~${p.askSoftHuman} soft, min ${MIN_ORDER_SIZE_USD} stable (feed ${FEED_BASE}?pair=${p.key})`
347367
)
348368
}
369+
console.log(
370+
` production-like defaults: ttl=${STITCH_LOCAL_TTL_SECS}s, ` +
371+
`refresh_threshold=${STITCH_LOCAL_REFRESH_THRESHOLD_BPS}bps, ` +
372+
`funding_headroom=${STITCH_LOCAL_FUNDING_HEADROOM}x, ` +
373+
`feed_staleness=${FEED_STALENESS_SECS}s`
374+
)
349375
console.log('')
350376

351377
// A bot with no reachable feed silently skips every pool ("feed fetch failed")
@@ -400,13 +426,12 @@ if (!feedUp) {
400426
}
401427
console.log(`Using price endpoint ${FEED_BASE}.\n`)
402428

403-
// Keep the oracles fresh for the whole session (they'd otherwise re-stale after
404-
// 30 min and the feed/UI would start reverting again).
429+
// Keep local oracles fresh inside the production-like feed staleness window.
405430
const refreshTimer = setInterval(
406431
() => {
407432
refreshOracles().catch(() => {})
408433
},
409-
20 * 60 * 1000
434+
ORACLE_REFRESH_SECONDS * 1000
410435
)
411436

412437
const bot = spawn(

0 commit comments

Comments
 (0)