From b4e9da9c4ac5c6955a7da78b996dddfa4de8cd71 Mon Sep 17 00:00:00 2001 From: Hui-Sang Kim <102507786+Hiksang@users.noreply.github.com> Date: Thu, 7 May 2026 02:06:01 +0900 Subject: [PATCH] fix(config): drop 5 malformed addresses + add shape-guard test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovered 2026-05-07 during the multi-chain `status --verify` sweep on HyperEVM: hybra.rewards_distributor was reported as `NO_CODE` even though hybra is otherwise healthy. Inspecting the TOML revealed the address was malformed — only 37 hex chars after `0x` (should be 40), i.e. data-entry truncation that pre-dated the active config baseline. A regex sweep across `ts/config/` then turned up four sibling cases in `nest.toml` (each a per-pool `gauge` field at 39 hex chars). Nest is currently `is_active = false` so those entries were filtered out of `getProtocolsForChain()` before reaching `status --verify`, but they were still ticking time bombs for the day someone re-activates. Fix: 1. `protocols/dex/hybra.toml`: drop the orphan `rewards_distributor` line (no adapter code referenced it; tried `rewards_distributor()`, `rewardsDistributor()`, `distributor()`, `rewardDistributor()` getters on the minter / voter / ve contracts — all reverted, so re-discovering the correct address is non-trivial and out of scope for this fix). 2. `protocols/dex/nest.toml`: drop the four malformed pool gauge fields. The pool addresses themselves stayed valid — only the gauge field was truncated. `PoolInfo.gauge` is optional, so the pools remain valid TOML entries. 3. New test `src/qa/address-shape.test.ts` walks every TOML file in `ts/config/` and rejects any `"0x..."` literal whose length is not exactly 42 (0x + 40 hex). Also includes a self-test for the regex so future refactors can't silently make the guard permissive. This is a config-only fix — no adapter behaviour changes. The removed entries were never readable by code in the first place (viem rejects sub-42-char addresses). Verified: - pnpm -C ts -r build — clean. - pnpm -C ts -r lint — 3 packages, tsc --noEmit clean. - pnpm -C ts -r test — defi-core 32/32, defi-protocols 43/43, defi-cli 89/89 (+2 address-shape tests). - The new shape-guard test passes; pre-fix it would have rejected all 5 of the addresses removed in this commit. Out of scope: - Re-running `status --verify` against a live RPC after the fix. The contract addresses left in hybra.toml are unchanged from the prior verified-79 set; only the malformed line was removed. --- ts/config/protocols/dex/hybra.toml | 4 +- ts/config/protocols/dex/nest.toml | 9 +- .../defi-cli/src/qa/address-shape.test.ts | 88 +++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 ts/packages/defi-cli/src/qa/address-shape.test.ts diff --git a/ts/config/protocols/dex/hybra.toml b/ts/config/protocols/dex/hybra.toml index c449168..6dcdc5a 100644 --- a/ts/config/protocols/dex/hybra.toml +++ b/ts/config/protocols/dex/hybra.toml @@ -22,7 +22,9 @@ v2_factory = "0x9c7397c9C5ecC400992843408D3A283fE9108009" voter = "0x5623F012d15EB828C12fe32e46d40AdC2A9e4FA3" ve_token = "0xAE6D5FcE541216BDA471D311425B5412D9f1DEb9" minter = "0xA8265e40e4Cdf6DB345861F4FCb75F9CC63E149b" -rewards_distributor = "0x04FCae9aF38e79B7bb82C6491BDACC2431F0e" +# rewards_distributor: removed 2026-05-07 — the previously-listed address +# was malformed (37 hex chars after 0x, should be 40) and no adapter code +# referenced it. Re-add with the correct address when it's actually wired in. gauge_factory_cl = "0xeB60888176d0c6AF4C539D64b2E83E470a63e4F9" gauge_manager = "0x742CAA5bA7C92ca6CFeBfD0e73c21739b3b65d5e" bribe_factory = "0x2555F79AC6e8096c755096e3A8d175a4Bf5fC82F" diff --git a/ts/config/protocols/dex/nest.toml b/ts/config/protocols/dex/nest.toml index bb79f2e..c345abf 100644 --- a/ts/config/protocols/dex/nest.toml +++ b/ts/config/protocols/dex/nest.toml @@ -67,7 +67,8 @@ address = "0x45FbF9786cDBDE9E940620F4Af0eb42B76848d17" token0 = "USDH" token1 = "WHYPE" tick_spacing = 10 -gauge = "0xDb57bF01A22D11F2c5eE6B7b4E5e2d36f6B34E6" +# gauge: removed 2026-05-07 — previously listed address was malformed +# (39 hex chars after 0x, should be 40); nest is is_active=false anyway. [[protocol.pools]] name = "WHYPE/kHYPE" @@ -83,7 +84,7 @@ address = "0x36De5262ADe55357A15226B85e2f20622D6eB203" token0 = "WHYPE" token1 = "PURR" tick_spacing = 50 -gauge = "0xb744C8Aa4F2f5E6B70c7Ab7A6A4F67a8C7B270F" +# gauge: removed 2026-05-07 — malformed address (39 hex chars). [[protocol.pools]] name = "WHYPE/wstHYPE" @@ -91,7 +92,7 @@ address = "0x728C8E4E89472B96Bd2B830438E0CB458F141db9" token0 = "WHYPE" token1 = "wstHYPE" tick_spacing = 1 -gauge = "0xcF5848f2D4e1a5cF2c8Ee8B72cB3E0fA1D7c1d9" +# gauge: removed 2026-05-07 — malformed address (39 hex chars). [[protocol.pools]] name = "WHYPE/LHYPE" @@ -123,4 +124,4 @@ address = "0xb09a299E9f7D333420d347EEBE0456Cb0f8545d5" token0 = "USDH" token1 = "USDT0" tick_spacing = 1 -gauge = "0x5A6D3E43c7F2F5C5d4EbA2b8A7cF8e6D1D1B2c1" +# gauge: removed 2026-05-07 — malformed address (39 hex chars). diff --git a/ts/packages/defi-cli/src/qa/address-shape.test.ts b/ts/packages/defi-cli/src/qa/address-shape.test.ts new file mode 100644 index 0000000..e7c1200 --- /dev/null +++ b/ts/packages/defi-cli/src/qa/address-shape.test.ts @@ -0,0 +1,88 @@ +// Address shape guard. +// +// Every "0x..." literal in the bundled config (chains.toml, tokens/*.toml, +// protocols/**/*.toml) must be exactly `0x` + 40 hex chars (20 bytes). +// On 2026-05-07 a sweep found five malformed entries that had been live +// for weeks: +// - protocols/dex/hybra.toml: rewards_distributor (37 hex chars) +// - protocols/dex/nest.toml: 4× pool gauge fields (39 hex chars each) +// `defi --chain hyperevm status --verify` flagged the hybra one as +// `NO_CODE` because it slipped past the 42-length check that gates +// `isPlaceholder()`. The four nest entries flew under the radar +// because nest is `is_active = false` and its protocol is filtered +// out by `getProtocolsForChain()` before status verifies anything. +// +// This test scans the raw TOML bytes directly so we catch shape +// regressions even on inactive entries. +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PKG_ROOT = resolve(__dirname, "../.."); +const TS_ROOT = resolve(PKG_ROOT, "../.."); +const CONFIG_DIR = resolve(TS_ROOT, "config"); + +function walkToml(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + const st = statSync(p); + if (st.isDirectory()) out.push(...walkToml(p)); + else if (p.endsWith(".toml")) out.push(p); + } + return out; +} + +// Match `"0x"` literals. Capturing group 1 is the address. +const ADDR_RE = /"(0x[0-9a-fA-F]+)"/g; + +interface Offender { + file: string; + line: number; + address: string; +} + +function findMalformedAddresses(): Offender[] { + const tomls = walkToml(CONFIG_DIR); + const out: Offender[] = []; + for (const file of tomls) { + const lines = readFileSync(file, "utf8").split("\n"); + for (let i = 0; i < lines.length; i++) { + ADDR_RE.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = ADDR_RE.exec(lines[i]))) { + const addr = m[1]; + if (addr.length !== 42) { + out.push({ file: file.replace(CONFIG_DIR + "/", ""), line: i + 1, address: addr }); + } + } + } + } + return out; +} + +describe("config TOML address shape", () => { + it("every \"0x...\" literal in config/ is exactly 42 chars (0x + 40 hex)", () => { + const offenders = findMalformedAddresses(); + const lines = offenders.map( + (o) => `${o.file}:${o.line} ${o.address} (length=${o.address.length}, expected=42)`, + ); + expect( + offenders, + "Malformed addresses found in config TOML — fix or remove:\n" + lines.join("\n"), + ).toEqual([]); + }); + + it("the regex itself catches the canonical bad shape (sanity)", () => { + // Self-test: feed the regex an artificial 41-char address and confirm + // the test would have rejected it. Guards against the regex silently + // becoming permissive after future refactors. + const sample = '"0x1234567890abcdef1234567890abcdef1234567"'; // 41 hex chars + ADDR_RE.lastIndex = 0; + const m = ADDR_RE.exec(sample); + expect(m).not.toBeNull(); + expect(m![1].length).not.toBe(42); + }); +});