LAUNCH-BLOCKER: Terra recipient address accepted without bech32 checksum validation #117

Closed
opened 2026-04-24 04:09:13 +00:00 by Brouie · 14 comments
Brouie commented 2026-04-24 04:09:13 +00:00 (Migrated from gitlab.com)

Found during Bridge #116 Section C verification.

Repro

  1. FROM: BNB Chain, TO: Terra Classic, token: testa (tokena-cb), amount 1.2
  2. Click 'Autofill with connected wallet' — populates valid Terra address terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wnt
  3. Edit last character: terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wny
  4. Field silently accepts. CTA stays active. No error.

Why launch-blocker

Terra bech32 addresses checksum-encode the last 6-8 chars. Typos silently accepted = funds bridge to wrong address, permanently lost.

Fix

bech32 decode + checksum validation on Terra recipient field.

cc @PlasticDigits

Found during Bridge #116 Section C verification. ## Repro 1. FROM: BNB Chain, TO: Terra Classic, token: testa (tokena-cb), amount 1.2 2. Click 'Autofill with connected wallet' — populates valid Terra address terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wnt 3. Edit last character: terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wny 4. Field silently accepts. CTA stays active. No error. ## Why launch-blocker Terra bech32 addresses checksum-encode the last 6-8 chars. Typos silently accepted = funds bridge to wrong address, permanently lost. ## Fix bech32 decode + checksum validation on Terra recipient field. cc @PlasticDigits
Brouie commented 2026-04-24 04:10:53 +00:00 (Migrated from gitlab.com)

image.png{width=528 height=600}

![image.png](/uploads/bcb1464612f9219f45637b6b95d7fea5/image.png){width=528 height=600}
Brouie commented 2026-04-24 04:37:48 +00:00 (Migrated from gitlab.com)

@PlasticDigits extending scope — same silent-typo bug confirmed for EVM recipient field.

Repro 2 (EVM side)

  1. FROM: Terra Classic, TO: BNB Chain, amount 1.2 testa
  2. Click 'Autofill with connected wallet' — populates 0xc46b15f4B56489a16F561c22D5F0BA8bdCa80650
  3. Edit last character: 0xc46b15f4B56489a16F561c22D5F0BA8bdCa80651
  4. Field silently accepts. CTA 'BRIDGE FROM TERRA' stays active. YOU WILL RECEIVE still calculates. No error.

EVM uses EIP-55 mixed-case checksum. Same class of validation gap — format-level regex accepts the string but no checksum verification on the address. Fix should cover Terra (bech32) AND EVM (EIP-55) AND Solana (base58 checksum test needed still) in one recipient-validation pass.

Screenshot attached on web UI.

@PlasticDigits extending scope — same silent-typo bug confirmed for EVM recipient field. ## Repro 2 (EVM side) 1. FROM: Terra Classic, TO: BNB Chain, amount 1.2 testa 2. Click 'Autofill with connected wallet' — populates `0xc46b15f4B56489a16F561c22D5F0BA8bdCa80650` 3. Edit last character: `0xc46b15f4B56489a16F561c22D5F0BA8bdCa80651` 4. Field silently accepts. CTA 'BRIDGE FROM TERRA' stays active. YOU WILL RECEIVE still calculates. No error. EVM uses EIP-55 mixed-case checksum. Same class of validation gap — format-level regex accepts the string but no checksum verification on the address. Fix should cover Terra (bech32) AND EVM (EIP-55) AND Solana (base58 checksum test needed still) in one recipient-validation pass. Screenshot attached on web UI.
Brouie commented 2026-04-24 04:38:31 +00:00 (Migrated from gitlab.com)

image.png{width=401 height=359}

![image.png](/uploads/400c34dd1974db5acf7ecbd3c496da70/image.png){width=401 height=359}
PlasticDigits commented 2026-04-24 05:41:11 +00:00 (Migrated from gitlab.com)

mentioned in commit b12879afec

mentioned in commit b12879afec45efac3166d46334cc8a9dc63811db
PlasticDigits commented 2026-04-24 05:41:41 +00:00 (Migrated from gitlab.com)

GL-117 fix merged to main (commit b12879a)

@brouie please verify after v0.1.83+ is deployed (or any build that includes this commit).

What changed (original + extended scope)

  • Terra (terra1…): BIP173 bech32 checksum is verified in hashVerification before decode; isValidTerraAddress / terraAddressToBytes32 reject checksum typos (e.g. last char edit in the GL repro).
  • EVM (0x…): isValidEvmAddress now uses viem isAddress(addr, { strict: true }) so EIP-55 is enforced when the user enters mixed case (catches the …50 → …51 repro).
  • Solana: Recipient validation uses isValidSolanaAddress (PublicKey / curve check); Solana→Terra deposits now encode terra1… via terraAddressToBytes32 (checksum-valid addresses only).
  • Bridge CTA: TransferForm disables submit when the recipient fails the route’s validator (not only inline field styling).

Docs / agents

Checklist for your verification

  1. EVM → Terra: Autofill Terra recipient, change last character to break bech32 → red border / “Invalid address”, Bridge button disabled, tooltip mentions bech32.
  2. Terra → BNB (or any EVM dest): Autofill EVM recipient, flip last hex digit to break EIP-55 (mixed-case input) → invalid + disabled CTA.
  3. Optional: All-lowercase EVM paste still accepted (EIP-55 optional for uniform case — documented in INV-RCP1).
  4. Solana routes: Invalid base58 / pubkey still blocked; Solana→Terra with valid terra1… still works.

Leaving issue open until you sign off on the deployed build.

## GL-117 fix merged to `main` (commit b12879a) @brouie please verify after **v0.1.83+** is deployed (or any build that includes this commit). ### What changed (original + extended scope) - **Terra (`terra1…`):** BIP173 **bech32 checksum** is verified in `hashVerification` before decode; `isValidTerraAddress` / `terraAddressToBytes32` reject checksum typos (e.g. last char edit in the GL repro). - **EVM (`0x…`):** `isValidEvmAddress` now uses **viem** `isAddress(addr, { strict: true })` so **EIP-55** is enforced when the user enters **mixed case** (catches the `…50` → `…51` repro). - **Solana:** Recipient validation uses `isValidSolanaAddress` (`PublicKey` / curve check); Solana→Terra deposits now encode `terra1…` via `terraAddressToBytes32` (checksum-valid addresses only). - **Bridge CTA:** `TransferForm` disables submit when the recipient fails the route’s validator (not only inline field styling). ### Docs / agents - Invariant: [`docs/FRONTEND_BRIDGE_INVARIANTS.md`](https://gitlab.com/PlasticDigits/cl8y-bridge-monorepo/-/blob/main/docs/FRONTEND_BRIDGE_INVARIANTS.md) (**INV-RCP1**) - Third-party agent notes: [`skills/agent-bridge-recipient-validation.md`](https://gitlab.com/PlasticDigits/cl8y-bridge-monorepo/-/blob/main/skills/agent-bridge-recipient-validation.md) ### Checklist for your verification 1. **EVM → Terra:** Autofill Terra recipient, change last character to break bech32 → red border / “Invalid address”, Bridge button **disabled**, tooltip mentions bech32. 2. **Terra → BNB (or any EVM dest):** Autofill EVM recipient, flip last hex digit to break EIP-55 (mixed-case input) → invalid + **disabled** CTA. 3. **Optional:** All-lowercase EVM paste still **accepted** (EIP-55 optional for uniform case — documented in INV-RCP1). 4. **Solana routes:** Invalid base58 / pubkey still blocked; Solana→Terra with valid `terra1…` still works. Leaving issue **open** until you sign off on the deployed build.
Brouie commented 2026-04-24 05:54:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #119

mentioned in issue #119
Brouie commented 2026-04-24 06:06:19 +00:00 (Migrated from gitlab.com)

mentioned in issue #116

mentioned in issue #116
Brouie commented 2026-04-24 06:14:49 +00:00 (Migrated from gitlab.com)

@PlasticDigits verification on the live fix — Terra and EVM both working beautifully:

Terra bech32 — PASS

  • terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wnt accepted
  • terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wny (last char swapped) → 'Invalid address' fires

EVM EIP-55 — PASS

  • Mixed-case corrupted address → 'Invalid EVM recipient address (check EIP-55 checksum if using mixed case)' fires with clear helper text
  • All-lowercase legacy address correctly accepted (per EIP-55 spec)

Solana — PARTIAL

  • Cu6Q7uU5qHsFuzAkrcxAz1xqrpUNB7vtEFNuSuQ1aDB0 (last char y→0) → 'Invalid Solana recipient address' fires ✅
  • Cu6Q7uU5qHsFuzAkrcxAz1xqrpUNB7vtEFNuSuQ1aDBo (last char y→o) → silently accepted, no error, CTA active

The first case is caught because 0 is not a valid base58 character (base58 excludes 0/O/I/l). The second case substitutes a valid base58 character but one that breaks the ed25519 public-key checksum — which doesn't appear to be validated.

Solana addresses are 32-byte ed25519 public keys base58-encoded. Format check alone is insufficient; need to verify the decoded bytes are a valid ed25519 point, or at minimum verify the base58-decoded length is exactly 32 bytes.

Screenshots

All 5 tests attached via web UI — Terra valid, Terra corrupted, EVM mixed-case corrupted, Solana with 0 swap (caught), Solana with o swap (not caught).

Closing scope: Solana validation still has the same class of typo gap as the original bug. Terra + EVM good.

@PlasticDigits verification on the live fix — Terra and EVM both working beautifully: ## Terra bech32 — PASS - `terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wnt` accepted - `terra17ks3ncgx9q4q9d2rpfv0uafs732derhxvx0wny` (last char swapped) → 'Invalid address' fires ## EVM EIP-55 — PASS - Mixed-case corrupted address → 'Invalid EVM recipient address (check EIP-55 checksum if using mixed case)' fires with clear helper text - All-lowercase legacy address correctly accepted (per EIP-55 spec) ## Solana — PARTIAL - `Cu6Q7uU5qHsFuzAkrcxAz1xqrpUNB7vtEFNuSuQ1aDB0` (last char `y`→`0`) → 'Invalid Solana recipient address' fires ✅ - `Cu6Q7uU5qHsFuzAkrcxAz1xqrpUNB7vtEFNuSuQ1aDBo` (last char `y`→`o`) → **silently accepted**, no error, CTA active The first case is caught because `0` is not a valid base58 character (base58 excludes 0/O/I/l). The second case substitutes a valid base58 character but one that breaks the ed25519 public-key checksum — which doesn't appear to be validated. Solana addresses are 32-byte ed25519 public keys base58-encoded. Format check alone is insufficient; need to verify the decoded bytes are a valid ed25519 point, or at minimum verify the base58-decoded length is exactly 32 bytes. ## Screenshots All 5 tests attached via web UI — Terra valid, Terra corrupted, EVM mixed-case corrupted, Solana with `0` swap (caught), Solana with `o` swap (not caught). Closing scope: Solana validation still has the same class of typo gap as the original bug. Terra + EVM good.
Brouie commented 2026-04-24 06:16:08 +00:00 (Migrated from gitlab.com)

image.png{width=617 height=600}

image.png{width=686 height=598}

image.png{width=585 height=600}

image.png{width=657 height=586}

image.png{width=551 height=561}

![image.png](/uploads/f1561c469c558fcb123cbc2ebe49fd50/image.png){width=617 height=600} ![image.png](/uploads/9d35db0fdac0a3090e8052d738d02414/image.png){width=686 height=598} ![image.png](/uploads/f44aa48695f0b16bd3034dcbee3ed258/image.png){width=585 height=600} ![image.png](/uploads/dafd5d48eb4f1813108fe6a5b3571464/image.png){width=657 height=586} ![image.png](/uploads/c22eef744d6dd4ff587423591a355799/image.png){width=551 height=561}
PlasticDigits commented 2026-04-24 06:47:08 +00:00 (Migrated from gitlab.com)

mentioned in commit 9ec963582b

mentioned in commit 9ec963582bbe883bf2b8d896e201bbd7fb033309
PlasticDigits commented 2026-04-24 06:47:19 +00:00 (Migrated from gitlab.com)

@brouie follow-up to your Solana — PARTIAL verification (y→o off-curve typo still accepted after GL-117 first pass).

What we changed

  • @solana/web3.js new PublicKey(string) only checks base58 decode to 32 bytes — it does not verify the point is on the ed25519 curve, so a last-character change can stay in the base58 alphabet and still decode (your …DBy → …DBo case).
  • isValidSolanaAddress now decodes the key and requires PublicKey.isOnCurve (same check as the Solana public API), so off-curve 32-byte strings are rejected.
  • solanaAddressToBytes32 / bytes32ToSolanaAddress use the same on-curve rule so encoding paths cannot smuggle an invalid recipient.
  • Terra → Solana encodeDestAccountBase64 Solana branch now uses solanaAddressToBytes32 + viem hexToBytes (single validation path).
  • TransferForm Solana-dest account encoding now gates on isValidSolanaAddress before new PublicKey (defense in depth if submit logic ever drifts).
  • Docs / agents: docs/FRONTEND_BRIDGE_INVARIANTS.md (INV-RCP1) and skills/agent-bridge-recipient-validation.md updated; unit tests in packages/frontend/src/services/solana/address.test.ts (includes your Brouie repro) + useTerraDeposit.test.ts.

Pushed to main: 9ec9635

Checklist to verify

  1. Solana dest / routes using Solana recipient: Enter Cu6Q7uU5qHsFuzAkrcxAz1xqrpUNB7vtEFNuSuQ1aDBy → field valid, CTA as expected.
  2. Last char y→0 (…DB0) → still rejected (non-base58).
  3. Last char y→o (…DBo) → Invalid Solana recipient address, CTA disabled (ed25519 off-curve).
  4. Terra → Solana flow: with invalid …DBo in the recipient field, native/CW20 deposit should not build a 32-byte dest from that string (form should block; encodeDestAccountBase64 throws the same class of error if invoked).
  5. Regression: 11111111111111111111111111111111 (system default) and normal wallets still accepted.

Leaving the issue open per process until you sign off on a build that includes this commit.

@brouie follow-up to your **Solana — PARTIAL** verification (y→`o` off-curve typo still accepted after GL-117 first pass). ## What we changed - `@solana/web3.js` `new PublicKey(string)` only checks **base58 decode to 32 bytes** — it does **not** verify the point is on the ed25519 curve, so a last-character change can stay in the base58 alphabet and still decode (your `…DBy` → `…DBo` case). - **`isValidSolanaAddress`** now decodes the key and requires **`PublicKey.isOnCurve`** (same check as the Solana public API), so off-curve 32-byte strings are rejected. - **`solanaAddressToBytes32` / `bytes32ToSolanaAddress`** use the same on-curve rule so encoding paths cannot smuggle an invalid recipient. - **Terra → Solana** `encodeDestAccountBase64` Solana branch now uses `solanaAddressToBytes32` + `viem` `hexToBytes` (single validation path). - **TransferForm** Solana-dest account encoding now gates on `isValidSolanaAddress` before `new PublicKey` (defense in depth if submit logic ever drifts). - **Docs / agents:** `docs/FRONTEND_BRIDGE_INVARIANTS.md` (INV-RCP1) and `skills/agent-bridge-recipient-validation.md` updated; unit tests in `packages/frontend/src/services/solana/address.test.ts` (includes your Brouie repro) + `useTerraDeposit.test.ts`. **Pushed to `main`:** 9ec9635 ## Checklist to verify 1. [ ] **Solana dest / routes using Solana recipient:** Enter `Cu6Q7uU5qHsFuzAkrcxAz1xqrpUNB7vtEFNuSuQ1aDBy` → field valid, CTA as expected. 2. [ ] **Last char y→0** (`…DB0`) → still rejected (non-base58). 3. [ ] **Last char y→o** (`…DBo`) → **Invalid Solana recipient address**, CTA **disabled** (ed25519 off-curve). 4. [ ] **Terra → Solana** flow: with invalid `…DBo` in the recipient field, native/CW20 deposit should not build a 32-byte dest from that string (form should block; `encodeDestAccountBase64` throws the same class of error if invoked). 5. [ ] **Regression:** `11111111111111111111111111111111` (system default) and normal wallets still accepted. Leaving the issue **open** per process until you sign off on a build that includes this commit.
PlasticDigits commented 2026-04-24 06:48:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 3d51a91958

mentioned in commit 3d51a9195866e8c97e189b9efaf6a0f4502835c4
PlasticDigits commented 2026-04-24 06:48:19 +00:00 (Migrated from gitlab.com)

Follow-up: docs/SOLANA_BRIDGE_INVARIANTS.md intro now cross-links to INV-RCP1 and skills/agent-bridge-recipient-validation.md (commit 3d51a91) so program-side docs point at the same GL-117 UI rule.

Follow-up: `docs/SOLANA_BRIDGE_INVARIANTS.md` intro now cross-links to **INV-RCP1** and `skills/agent-bridge-recipient-validation.md` (commit 3d51a91) so program-side docs point at the same GL-117 UI rule.
Brouie commented 2026-04-26 04:14:08 +00:00 (Migrated from gitlab.com)

@PlasticDigits closing on cb7f871. Solana follow-up shipped.

Live mainnet from Friday already covered Terra bech32 + EVM EIP-55 (PASS for both with chain-specific helper text). Solana piece is what was still open from my Friday note (y to o silently accepted because o stays in base58 alphabet but decodes off-curve).

Fix at services/solana/address.ts: parseOnCurveUserPubkeyBase58 wraps PublicKey + PublicKey.isOnCurve. Off-curve points throw, so isValidSolanaAddress returns false on the y to o repro.

Test at services/solana/address.test.ts (5/5 passing): includes my exact Cu6Q...aDBy to aDBo case with explicit assertion that PublicKey.isOnCurve(badO) is false. Non-base58 case (aDB0) also covered.

Validator wired into:

  • RecipientInput.tsx L32 (field-level validation)
  • TransferForm.tsx L678 (CTA enable gate)
  • TransferForm.tsx L1548/1595/1684 (submit-path guards on all 3 submission codepaths)

Both my Friday repros now reject:

  • Cu6Q...aDB0 (last char 0, not in base58 alphabet) - rejects via base58 decode
  • Cu6Q...aDBo (last char o, valid base58 but off-curve) - rejects via isOnCurve

Closing.

@PlasticDigits closing on cb7f871. Solana follow-up shipped. Live mainnet from Friday already covered Terra bech32 + EVM EIP-55 (PASS for both with chain-specific helper text). Solana piece is what was still open from my Friday note (y to o silently accepted because o stays in base58 alphabet but decodes off-curve). Fix at services/solana/address.ts: parseOnCurveUserPubkeyBase58 wraps PublicKey + PublicKey.isOnCurve. Off-curve points throw, so isValidSolanaAddress returns false on the y to o repro. Test at services/solana/address.test.ts (5/5 passing): includes my exact Cu6Q...aDBy to aDBo case with explicit assertion that PublicKey.isOnCurve(badO) is false. Non-base58 case (aDB0) also covered. Validator wired into: - RecipientInput.tsx L32 (field-level validation) - TransferForm.tsx L678 (CTA enable gate) - TransferForm.tsx L1548/1595/1684 (submit-path guards on all 3 submission codepaths) Both my Friday repros now reject: - Cu6Q...aDB0 (last char 0, not in base58 alphabet) - rejects via base58 decode - Cu6Q...aDBo (last char o, valid base58 but off-curve) - rejects via isOnCurve Closing.
Brouie (Migrated from gitlab.com) closed this issue 2026-04-26 04:14:56 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#117
No description provided.