security(solana): bind withdraw_execute TokenMapping to submit-time src_token #181

Open
opened 2026-09-12 12:33:25 +00:00 by PlasticDigits · 0 comments

Summary

Solana withdraw_submit pins TokenMapping by PDA seeds (src_chain, src_token) and copies only local_mint onto PendingWithdraw.token. PendingWithdraw does not store src_token (or the mapping pubkey). withdraw_execute re-derives the mapping from (pending_withdraw.src_chain, token_mapping.dest_token) and only requires mint == pw.token == token_mapping.local_mint. The V2 hash still validates because it hashes the destination mint, not the remote src_token.

If two mappings share the same local_mint on one src_chain (different remote dest_token and/or TokenMode), execute can run through the alternate mapping (MintBurn vs LockUnlock) without HashMismatch.

This is not #176 (1-of-1 operator approve). This is not #180 (Terra pending src_decimals after approve). This is not closed #104 (token_mapping uninitialized at execute). Those share withdraw / mapping words. The bug is execute-time mapping substitution plus no uniqueness of local_mint per src_chain.

Internal review id: SOL-H2 (high). Still in source as of 2026-09-12.

Bundle (same ticket, do not split):

  1. Persist submit-time src_token (or the mapping identity) on PendingWithdraw.
  2. withdraw_execute must seed TokenMapping from that stored identity, not from the passed account’s dest_token field.
  3. register_token must reject a second mapping for the same (src_chain / dest_chain, local_mint).
  4. Tests that execute with an alternate mapping (same mint, different remote token / TokenMode) reverts.

Founder-required Solana program. No community autoland. Do not add ready.

Impact (today vs hypothetical)

Funds at risk in source whenever two TokenMapping PDAs exist for the same remote chain and the same SPL local_mint. register_token is admin-only and keys PDAs by (dest_chain, dest_token) only — it never uniqueness-checks local_mint. A later second register (confused ops, or a hostile admin) is enough; no user can create the second mapping.

Once those two PDAs exist, execute is not admin-gated: the recipient supplies accounts. They can pass the mapping whose mode is MintBurn instead of the submit-time LockUnlock (unbacked mint_to) or the reverse (unlock from the bridge ATA for a mint/burn route). compute_transfer_hash does not include remote src_token (INV-H2: destination token word). Hash recompute (INV-W2) therefore does not detect the swap.

Not a permissionless drain on a deployment whose live mappings already have unique (chain, local_mint). Rate limits (INV-W4) bound size; they do not bind mode. Sticky until the program is upgraded (pending account space / seed change). In-flight pending PDAs stay on the old layout until they execute or cancel.

Hypothetical-only if every live program has never had two mappings to one mint and admin will never register a second. Source still allows both.

Do not publish a mainnet submit-then-swap-mapping-then-execute sequence.

SOL-H2: submit binds src_token; execute does not

withdraw_submit (packages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_submit.rs):

  • Seeds token_mapping with [b"token", params.src_chain, params.src_token].
  • Constraint: token_mapping.local_mint == params.dest_token (TokenMappingMismatch).
  • Writes pw.token = params.dest_token, copies tm.src_decimals / tm.decimals, does not write src_token.

PendingWithdraw (state/pending_withdraw.rs) fields: transfer_hash, src_chain, src_account, dest_account, token (local mint), amount, nonce, decimals, operator gas, flags, bump. No remote token id. No mapping pubkey.

withdraw_execute (instructions/withdraw_execute.rs):

  • Seeds token_mapping with [b"token", pending_withdraw.src_chain, token_mapping.dest_token]. The third seed is taken from the passed mapping account, so any valid mapping PDA for that src_chain satisfies seeds.
  • mint.key() == token_mapping.local_mint and pw.token == mint.key(). Same mint, different remote dest_token, still passes.
  • CPI follows token_mapping.mode (LockUnlock → transfer_checked from bridge ATA; MintBurn → mint_to). Payout size uses stored pw.src_decimals / pw.dest_decimals, not the execute-time mapping decimals — so the hole is mode / vault vs mint, not a second decimal rewrite (that is #180 on Terra).

register_token inits [b"token", dest_chain, dest_token] with no index PDA on local_mint. Two remote tokens can point at one SPL mint with different TokenMode. MintBurn still requires bridge mint authority at register; that does not stop a second mapping onto a mint the bridge already controls.

Audit tests cover honest LockUnlock vs MintBurn deposits (security_audit.test.ts §20) and PDA confusion for wrong chain / wrong hash / wrong mint. They do not submit against mapping A and execute against mapping B with the same local_mint. INV-W2 / INV-D1 docs say execute binds mint to the pending record; they do not say execute binds the same mapping PDA used at submit.

Why the new implementation is needed

  1. Submit already chose the remote token and therefore the mode, decimals snapshot, and vault-vs-mint path. Execute must not let the caller pick a sibling mapping that shares the mint.
  2. Hash cannot grow a src_token word (INV-H1 / EVM / Terra parity). Freeze the mapping identity on the pending account instead.
  3. Uniqueness of local_mint per remote chain is the second lock: even a patched execute should not allow two live PDAs that make substitution possible after a future seed mistake.
  4. Existing tests prove the honest mode paths and omit substitution. That locks in the hole.

Constraints / guardrails

  • Do not change V2 compute_transfer_hash layout. Do not put src_token into the 224-byte digest. Do not break EVM / Terra hash goldens.
  • Do not weaken pause, approve, canceler, delay, (src_chain, nonce) replay, rate limits, pw.token == mint, or WrongRecipient.
  • Execute seeds must be [TokenMapping::SEED, pw.src_chain, pw.src_token] (or equivalent stored mapping identity), not token_mapping.dest_token from the remaining account. Also require token_mapping.dest_token == pw.src_token and token_mapping.local_mint == pw.token.
  • register_token: reject if a mapping for (dest_chain, local_mint) already exists (init an index PDA, or scan is not acceptable on-chain). Same mint on a different dest chain remains allowed. Do not remove admin-only auth.
  • PendingWithdraw layout: adding [u8; 32] (and bump/space) is in scope. New submits must allocate the new INIT_SPACE. In-flight old-size PDAs: fail closed on execute (cannot bind src_token) or a one-shot migrate that copies identity from the original mapping without letting the caller name a different mapping. Document the choice. Do not silently keep the circular dest_token seed for old rows.
  • withdraw_execute_native does not take a mapping (native sentinel + LockUnlock only). Out of scope except: do not introduce a native substitution path; uniqueness should still forbid a second native mapping for the same chain if native uses Pubkey::default().
  • Operator writer / frontend: pass the submit-time mapping PDA at execute; do not “rediscover” by mint. Tests/docs must not claim mint-only lookup is sufficient.
  • Founder-required program / admin key. No community autoland. Do not add ready. No public mainnet substitution recipe.

Relevant files

Path Why
packages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_submit.rs Pins mapping by src_token; does not persist it
packages/contracts-solana/programs/cl8y-bridge/src/state/pending_withdraw.rs No src_token / mapping identity field
packages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_execute.rs Seeds mapping from token_mapping.dest_token; CPI uses mode
packages/contracts-solana/programs/cl8y-bridge/src/instructions/register_token.rs No local_mint uniqueness per chain
packages/contracts-solana/programs/cl8y-bridge/src/state/token_registry.rs TokenMapping / TokenMode
packages/contracts-solana/programs/cl8y-bridge/src/hash.rs Destination-token word only (do not change)
packages/contracts-solana/tests/security_audit.test.ts PDA substitution + token mode; add execute mapping swap
packages/contracts-solana/tests/spl_security.test.ts Honest mint/burn execute; add reject-alternate-mapping
docs/SOLANA_BRIDGE_INVARIANTS.md INV-W2 / INV-D1 omit mapping identity freeze
  1. Add src_token: [u8; 32] to PendingWithdraw. withdraw_submit copies params.src_token (and keep decimal snapshot from that mapping).
  2. Change WithdrawExecute mapping seeds to [TokenMapping::SEED, pending_withdraw.src_chain.as_ref(), pending_withdraw.src_token.as_ref()]. Keep mint == local_mint == pw.token.
  3. Add a local_mint uniqueness PDA (e.g. seeds [b"mint-map", dest_chain, local_mint]) created in register_token. Second register for that pair reverts (TokenMappingMismatch or a dedicated error).
  4. Tests: two mappings, same mint, different dest_token and TokenMode; submit via A; execute with B must fail; execute with A still unlocks/mints as registered. Duplicate register_token reverts. Honest single-mapping flow stays green.

Acceptance criteria

  • AC1. After submit against mapping (src_chain, src_token_A), withdraw_execute with mapping (src_chain, src_token_B) that shares local_mint reverts (seeds / mismatch). Hash still matches. No mint_to / unlock via B.
  • AC2. Execute with the submit-time mapping still pays via that mapping’s TokenMode after approve + delay.
  • AC3. register_token for an existing (dest_chain, local_mint) with a different dest_token reverts. Distinct mints on the same chain still register.
  • AC4. Pause / approve / cancel / delay / rate limit / hash goldens unchanged. cargo test in the Solana program and the documented Anchor suite still pass for existing honest paths.
  • AC5. Invariants doc states: execute mapping PDA is the submit-time (src_chain, src_token); local_mint is unique per remote chain.
  • AC6. In-flight pending behavior is documented and fail-closed (no unbounded old-layout substitution).

Test plan (functional paths)

# Path Expect
T1 Submit with mapping A; query pending token == A.local_mint; stored src_token == A.dest_token
T2 Approve + delay + execute with mapping A Succeeds; CPI matches A.mode
T3 Same pending, execute accounts pass mapping B (same mint) Revert (constraint / mismatch)
T4 register_token B after A, same local_mint, same dest chain Revert
T5 register_token same mint, other dest chain Allowed
T6 Honest single mapping LockUnlock execute Existing spl_security unlock path
T7 Honest single mapping MintBurn execute Existing remint-net path
T8 Submit src_token that does not match mapping seeds TokenMappingMismatch / seeds (unchanged)

Test plan (attack, hack, and abuse)

Non-exploitative. Local validator / Anchor tests only. Do not use these as a mainnet recipe.

# Vector Expect
A1 Two mappings, same mint, A=LockUnlock, B=MintBurn; submit A, execute B Revert; supply and bridge ATA unchanged
A2 Submit B (MintBurn), execute A (LockUnlock) Revert; no vault debit for a mint/burn pending
A3 Two LockUnlock mappings, same mint, different remote tokens; execute the other Revert (identity bind even when mode matches)
A4 Pass a mapping for another src_chain with the same mint Seeds fail
A5 Tamper pending (if fixture allows) so hash still matches but mapping B is used Still revert on mapping bind
A6 Second register_token after A already live Rejected; substitution fixture cannot be created after uniqueness ships

Verification criteria

  • Anchor tests T1–T8 and A1–A6 (new cases in security_audit.test.ts and/or spl_security.test.ts).
  • Grep: withdraw_execute mapping seeds include pending_withdraw.src_token (or the documented stored identity). register_token creates/checks a local_mint uniqueness PDA.
  • docs/SOLANA_BRIDGE_INVARIANTS.md INV-W2 / INV-D1 mention mapping identity freeze. SPL audit class 4 (wrong mint / mapping) cites the new reject-alternate-mapping test.
  • Do not verify by executing a substituted mapping on mainnet.

Out of scope

  • #176 operator M-of-N approve.
  • #180 Terra src_decimals admin rewrite.
  • EVM TokenRegistry uniqueness (separate stack).
  • Operator/canceler off-chain monitors; frontend UX except passing the correct mapping account.
  • Live program deploy / key rotation (ops).
  • Changing hash layout to include src_token.

First-pass model recommendation

Recommendation: grok-high

Rationale: Security class plus founder-required Solana program (withdraw_execute CPI mode, PendingWithdraw account layout, mapping PDA seeds, register_token uniqueness). Composer is disallowed (High/security; contracts / keys). Not a local three-file tweak: submit persist, execute seeds, register index, in-flight layout, tests, invariants. A wrong remaining-account seed (token_mapping.dest_token) leaves substitution open. Verify with Anchor duplicate-mapping fixtures, not a live mainnet execute.

## Summary Solana `withdraw_submit` pins `TokenMapping` by PDA seeds `(src_chain, src_token)` and copies only `local_mint` onto `PendingWithdraw.token`. `PendingWithdraw` does not store `src_token` (or the mapping pubkey). `withdraw_execute` re-derives the mapping from `(pending_withdraw.src_chain, token_mapping.dest_token)` and only requires `mint == pw.token == token_mapping.local_mint`. The V2 hash still validates because it hashes the **destination** mint, not the remote `src_token`. If two mappings share the same `local_mint` on one `src_chain` (different remote `dest_token` and/or `TokenMode`), execute can run through the **alternate** mapping (`MintBurn` vs `LockUnlock`) without `HashMismatch`. This is not [#176](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/176) (1-of-1 operator approve). This is not [#180](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/180) (Terra pending `src_decimals` after approve). This is not closed [#104](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/104) (`token_mapping` uninitialized at execute). Those share withdraw / mapping words. The bug is execute-time mapping **substitution** plus no uniqueness of `local_mint` per `src_chain`. Internal review id: SOL-H2 (high). Still in source as of 2026-09-12. Bundle (same ticket, do not split): 1. Persist submit-time `src_token` (or the mapping identity) on `PendingWithdraw`. 2. `withdraw_execute` must seed `TokenMapping` from that stored identity, not from the passed account’s `dest_token` field. 3. `register_token` must reject a second mapping for the same `(src_chain / dest_chain, local_mint)`. 4. Tests that execute with an alternate mapping (same mint, different remote token / `TokenMode`) reverts. Founder-required Solana program. No community autoland. Do not add `ready`. ## Impact (today vs hypothetical) Funds at risk **in source** whenever two `TokenMapping` PDAs exist for the same remote chain and the same SPL `local_mint`. `register_token` is admin-only and keys PDAs by `(dest_chain, dest_token)` only — it never uniqueness-checks `local_mint`. A later second register (confused ops, or a hostile admin) is enough; no user can create the second mapping. Once those two PDAs exist, execute is **not** admin-gated: the recipient supplies accounts. They can pass the mapping whose `mode` is `MintBurn` instead of the submit-time `LockUnlock` (unbacked `mint_to`) or the reverse (unlock from the bridge ATA for a mint/burn route). `compute_transfer_hash` does not include remote `src_token` (INV-H2: destination token word). Hash recompute (INV-W2) therefore does not detect the swap. Not a permissionless drain on a deployment whose live mappings already have unique `(chain, local_mint)`. Rate limits (INV-W4) bound size; they do not bind mode. Sticky until the program is upgraded (pending account space / seed change). In-flight pending PDAs stay on the old layout until they execute or cancel. Hypothetical-only if every live program has never had two mappings to one mint **and** admin will never register a second. Source still allows both. Do not publish a mainnet submit-then-swap-mapping-then-execute sequence. ### SOL-H2: submit binds `src_token`; execute does not `withdraw_submit` (`packages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_submit.rs`): - Seeds `token_mapping` with `[b"token", params.src_chain, params.src_token]`. - Constraint: `token_mapping.local_mint == params.dest_token` (`TokenMappingMismatch`). - Writes `pw.token = params.dest_token`, copies `tm.src_decimals` / `tm.decimals`, does **not** write `src_token`. `PendingWithdraw` (`state/pending_withdraw.rs`) fields: `transfer_hash`, `src_chain`, `src_account`, `dest_account`, `token` (local mint), `amount`, `nonce`, decimals, operator gas, flags, bump. No remote token id. No mapping pubkey. `withdraw_execute` (`instructions/withdraw_execute.rs`): - Seeds `token_mapping` with `[b"token", pending_withdraw.src_chain, token_mapping.dest_token]`. The third seed is taken from the **passed** mapping account, so any valid mapping PDA for that `src_chain` satisfies seeds. - `mint.key() == token_mapping.local_mint` and `pw.token == mint.key()`. Same mint, different remote `dest_token`, still passes. - CPI follows `token_mapping.mode` (`LockUnlock` → `transfer_checked` from bridge ATA; `MintBurn` → `mint_to`). Payout size uses **stored** `pw.src_decimals` / `pw.dest_decimals`, not the execute-time mapping decimals — so the hole is mode / vault vs mint, not a second decimal rewrite (that is [#180](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/180) on Terra). `register_token` inits `[b"token", dest_chain, dest_token]` with no index PDA on `local_mint`. Two remote tokens can point at one SPL mint with different `TokenMode`. MintBurn still requires bridge mint authority at register; that does not stop a second mapping onto a mint the bridge already controls. Audit tests cover honest LockUnlock vs MintBurn **deposits** (`security_audit.test.ts` §20) and PDA confusion for **wrong chain / wrong hash / wrong mint**. They do not submit against mapping A and execute against mapping B with the same `local_mint`. INV-W2 / INV-D1 docs say execute binds mint to the pending record; they do not say execute binds the **same mapping PDA** used at submit. ## Why the new implementation is needed 1. Submit already chose the remote token and therefore the mode, decimals snapshot, and vault-vs-mint path. Execute must not let the caller pick a sibling mapping that shares the mint. 2. Hash cannot grow a `src_token` word (INV-H1 / EVM / Terra parity). Freeze the mapping identity on the pending account instead. 3. Uniqueness of `local_mint` per remote chain is the second lock: even a patched execute should not allow two live PDAs that make substitution possible after a future seed mistake. 4. Existing tests prove the honest mode paths and omit substitution. That locks in the hole. ## Constraints / guardrails - Do not change V2 `compute_transfer_hash` layout. Do not put `src_token` into the 224-byte digest. Do not break EVM / Terra hash goldens. - Do not weaken pause, approve, canceler, delay, `(src_chain, nonce)` replay, rate limits, `pw.token == mint`, or `WrongRecipient`. - Execute seeds must be `[TokenMapping::SEED, pw.src_chain, pw.src_token]` (or equivalent stored mapping identity), **not** `token_mapping.dest_token` from the remaining account. Also require `token_mapping.dest_token == pw.src_token` and `token_mapping.local_mint == pw.token`. - `register_token`: reject if a mapping for `(dest_chain, local_mint)` already exists (init an index PDA, or scan is not acceptable on-chain). Same mint on a **different** dest chain remains allowed. Do not remove admin-only auth. - `PendingWithdraw` layout: adding `[u8; 32]` (and bump/space) is in scope. New submits must allocate the new `INIT_SPACE`. In-flight old-size PDAs: fail closed on execute (cannot bind `src_token`) or a one-shot migrate that copies identity from the original mapping **without** letting the caller name a different mapping. Document the choice. Do not silently keep the circular `dest_token` seed for old rows. - `withdraw_execute_native` does not take a mapping (native sentinel + `LockUnlock` only). Out of scope except: do not introduce a native substitution path; uniqueness should still forbid a second native mapping for the same chain if native uses `Pubkey::default()`. - Operator writer / frontend: pass the submit-time mapping PDA at execute; do not “rediscover” by mint. Tests/docs must not claim mint-only lookup is sufficient. - Founder-required program / admin key. No community autoland. Do not add `ready`. No public mainnet substitution recipe. ## Relevant files | Path | Why | | --- | --- | | `packages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_submit.rs` | Pins mapping by `src_token`; does not persist it | | `packages/contracts-solana/programs/cl8y-bridge/src/state/pending_withdraw.rs` | No `src_token` / mapping identity field | | `packages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_execute.rs` | Seeds mapping from `token_mapping.dest_token`; CPI uses `mode` | | `packages/contracts-solana/programs/cl8y-bridge/src/instructions/register_token.rs` | No `local_mint` uniqueness per chain | | `packages/contracts-solana/programs/cl8y-bridge/src/state/token_registry.rs` | `TokenMapping` / `TokenMode` | | `packages/contracts-solana/programs/cl8y-bridge/src/hash.rs` | Destination-token word only (do not change) | | `packages/contracts-solana/tests/security_audit.test.ts` | PDA substitution + token mode; add execute mapping swap | | `packages/contracts-solana/tests/spl_security.test.ts` | Honest mint/burn execute; add reject-alternate-mapping | | `docs/SOLANA_BRIDGE_INVARIANTS.md` | INV-W2 / INV-D1 omit mapping identity freeze | ## Recommended direction 1. Add `src_token: [u8; 32]` to `PendingWithdraw`. `withdraw_submit` copies `params.src_token` (and keep decimal snapshot from that mapping). 2. Change `WithdrawExecute` mapping seeds to `[TokenMapping::SEED, pending_withdraw.src_chain.as_ref(), pending_withdraw.src_token.as_ref()]`. Keep `mint == local_mint == pw.token`. 3. Add a `local_mint` uniqueness PDA (e.g. seeds `[b"mint-map", dest_chain, local_mint]`) created in `register_token`. Second register for that pair reverts (`TokenMappingMismatch` or a dedicated error). 4. Tests: two mappings, same mint, different `dest_token` and `TokenMode`; submit via A; execute with B must fail; execute with A still unlocks/mints as registered. Duplicate `register_token` reverts. Honest single-mapping flow stays green. ## Acceptance criteria - AC1. After submit against mapping `(src_chain, src_token_A)`, `withdraw_execute` with mapping `(src_chain, src_token_B)` that shares `local_mint` reverts (seeds / mismatch). Hash still matches. No mint_to / unlock via B. - AC2. Execute with the submit-time mapping still pays via that mapping’s `TokenMode` after approve + delay. - AC3. `register_token` for an existing `(dest_chain, local_mint)` with a different `dest_token` reverts. Distinct mints on the same chain still register. - AC4. Pause / approve / cancel / delay / rate limit / hash goldens unchanged. `cargo test` in the Solana program and the documented Anchor suite still pass for existing honest paths. - AC5. Invariants doc states: execute mapping PDA is the submit-time `(src_chain, src_token)`; `local_mint` is unique per remote chain. - AC6. In-flight pending behavior is documented and fail-closed (no unbounded old-layout substitution). ## Test plan (functional paths) | # | Path | Expect | | --- | --- | --- | | T1 | Submit with mapping A; query pending | `token == A.local_mint`; stored `src_token == A.dest_token` | | T2 | Approve + delay + execute with mapping A | Succeeds; CPI matches A.mode | | T3 | Same pending, execute accounts pass mapping B (same mint) | Revert (constraint / mismatch) | | T4 | `register_token` B after A, same `local_mint`, same dest chain | Revert | | T5 | `register_token` same mint, **other** dest chain | Allowed | | T6 | Honest single mapping LockUnlock execute | Existing spl_security unlock path | | T7 | Honest single mapping MintBurn execute | Existing remint-net path | | T8 | Submit `src_token` that does not match mapping seeds | `TokenMappingMismatch` / seeds (unchanged) | ## Test plan (attack, hack, and abuse) Non-exploitative. Local validator / Anchor tests only. Do not use these as a mainnet recipe. | # | Vector | Expect | | --- | --- | --- | | A1 | Two mappings, same mint, A=`LockUnlock`, B=`MintBurn`; submit A, execute B | Revert; supply and bridge ATA unchanged | | A2 | Submit B (MintBurn), execute A (LockUnlock) | Revert; no vault debit for a mint/burn pending | | A3 | Two LockUnlock mappings, same mint, different remote tokens; execute the other | Revert (identity bind even when mode matches) | | A4 | Pass a mapping for another `src_chain` with the same mint | Seeds fail | | A5 | Tamper pending (if fixture allows) so hash still matches but mapping B is used | Still revert on mapping bind | | A6 | Second `register_token` after A already live | Rejected; substitution fixture cannot be created after uniqueness ships | ## Verification criteria - Anchor tests T1–T8 and A1–A6 (new cases in `security_audit.test.ts` and/or `spl_security.test.ts`). - Grep: `withdraw_execute` mapping seeds include `pending_withdraw.src_token` (or the documented stored identity). `register_token` creates/checks a `local_mint` uniqueness PDA. - `docs/SOLANA_BRIDGE_INVARIANTS.md` INV-W2 / INV-D1 mention mapping identity freeze. SPL audit class 4 (wrong mint / mapping) cites the new reject-alternate-mapping test. - Do not verify by executing a substituted mapping on mainnet. ## Out of scope - [#176](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/176) operator M-of-N approve. - [#180](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/180) Terra `src_decimals` admin rewrite. - EVM `TokenRegistry` uniqueness (separate stack). - Operator/canceler off-chain monitors; frontend UX except passing the correct mapping account. - Live program deploy / key rotation (ops). - Changing hash layout to include `src_token`. ## First-pass model recommendation Recommendation: grok-high Rationale: Security class plus founder-required Solana program (`withdraw_execute` CPI mode, `PendingWithdraw` account layout, mapping PDA seeds, `register_token` uniqueness). Composer is disallowed (High/security; contracts / keys). Not a local three-file tweak: submit persist, execute seeds, register index, in-flight layout, tests, invariants. A wrong remaining-account seed (`token_mapping.dest_token`) leaves substitution open. Verify with Anchor duplicate-mapping fixtures, not a live mainnet execute.
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#181
No description provided.