Skip to content

Commit d39ab36

Browse files
committed
test(thorchain): Avalanche router deposit clear-signs; unpinned chain gated
The router pin is now (chain_id, address): an AVAX depositWithExpiry to the live-verified Avalanche router (00dc61..f1d4) must clear-sign with AdvancedMode OFF — the signature is ECDSA-recovered against the host-built EIP-155 pre-image over chainId 43114, and the native amount screen shows msg.value with the chain's ticker (AVAX). A deposit-shaped tx on a chain with no pinned router (BSC) falls to the blind-sign gate. Report catalog: E23 (AVAX clear-sign, with OLED capture) + E24 (unpinned-chain gate).
1 parent 71d6086 commit d39ab36

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

scripts/generate-test-report.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,23 @@ def _arg_shown(a):
902902
'through to the ordinary blind-sign gate instead of being silently native-decoded — the '
903903
'fix for the router-spoofing / blind-sign-bypass class of attack.',
904904
['Blind sign disabled (Blocked)']),
905+
('E23', 'test_msg_ethereum_thorchain_deposit',
906+
'test_deposit_with_expiry_avalanche_router',
907+
'THORChain deposit on Avalanche clear-signs (per-chain router pin)',
908+
'THORChain deploys its router at a DIFFERENT address on every EVM chain, so the pin is '
909+
'(chain_id, address) together. Before the chain scope, only mainnet deposits ever '
910+
'matched and an AVAX->ETH swap fell into the blind-sign gate. The Avalanche C-Chain '
911+
'router (00dc61..f1d4) is verified live against THORChain /inbound_addresses; the '
912+
'native amount screen shows msg.value with the CHAIN\'s ticker (AVAX), and the '
913+
'signature is ECDSA-recovered against the host-built pre-image over chainId 43114.',
914+
['Thorchain router screen', 'AVAX amount', 'Full memo']),
915+
('E24', 'test_msg_ethereum_thorchain_deposit',
916+
'test_deposit_unpinned_chain_blind_sign_blocked',
917+
'Deposit on an unpinned chain is blind-sign gated',
918+
'The mainnet router ADDRESS on a chain with no pinned router (BSC) must not inherit '
919+
'the deposit UX — the same address on another chain may hold unrelated attacker code. '
920+
'Falls to the AdvancedMode gate; rejection is pre-UI (no frame).',
921+
[]),
905922
]),
906923

907924
('R', 'Ripple (XRP)', '7.0.0',

tests/test_msg_ethereum_thorchain_deposit.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121

2222
THOR_ROUTER = "d37bbe5744d730a1d98d8dc97c42f0ca46ad7146" # ETH THORChain router
23+
THOR_ROUTER_AVAX = "00dc6100103bc402d490aee3f9a5560cbd91f1d4" # Avalanche C-Chain router
2324
ETH_NATIVE = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # sentinel for native ETH
2425

2526

@@ -137,6 +138,75 @@ def test_deposit_with_expiry_non_thor_address_blind_sign_blocked(self):
137138
data=data,
138139
)
139140

141+
def test_deposit_with_expiry_avalanche_router(self):
142+
"""A THORChain deposit on Avalanche clear-signs — the router pin is
143+
(chain_id, address), not Ethereum-mainnet-only.
144+
145+
Before the per-chain pin, thor_isThorchainTx only ever matched the
146+
mainnet router, so an AVAX->ETH swap fell into the AdvancedMode
147+
blind-sign gate and the device returned a bare ActionCancelled. The
148+
signature is ECDSA-recovered against the host-built EIP-155 pre-image,
149+
so a wrong digest, chain id, or key fails — not just a shape check.
150+
The native amount screen shows msg.value with the CHAIN's ticker
151+
(AVAX), never the mainnet pseudo-token's ETH label.
152+
"""
153+
self.requires_fullFeature()
154+
self.requires_firmware("7.15.0")
155+
self.setup_mnemonic_allallall()
156+
157+
from keepkeylib.signed_metadata import eth_sighash_legacy, keccak256
158+
159+
memo = "=:ETH.ETH:0xabcdef1234567890abcdef1234567890abcdef12:0:t:0"
160+
data = _build_deposit_with_expiry_calldata(memo)
161+
162+
n = parse_path("m/44'/60'/0'/0/0")
163+
nonce, gas_price, gas_limit = 4, 50000000000, 300000
164+
to = binascii.unhexlify(THOR_ROUTER_AVAX)
165+
value = 500000000000000000 # 0.5 AVAX (native = msg.value)
166+
chain_id = 43114
167+
168+
# AdvancedMode intentionally OFF — the deposit must clear-sign.
169+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
170+
n=n, nonce=nonce, gas_price=gas_price, gas_limit=gas_limit,
171+
to=to, value=value, chain_id=chain_id, data=data,
172+
)
173+
self.assertIn(sig_v, [2 * chain_id + 35, 2 * chain_id + 36])
174+
digest = eth_sighash_legacy(nonce, gas_price, gas_limit, to, value,
175+
data, chain_id)
176+
from ecdsa import VerifyingKey, SECP256k1, util
177+
rec = sig_v - (35 + 2 * chain_id)
178+
keys = VerifyingKey.from_public_key_recovery_with_digest(
179+
sig_r + sig_s, digest, SECP256k1, hashfunc=None,
180+
sigdecode=util.sigdecode_string,
181+
)
182+
signer = keccak256(keys[rec].to_string())[-20:]
183+
self.assertEqual(signer, self.client.ethereum_get_address(n))
184+
185+
def test_deposit_unpinned_chain_blind_sign_blocked(self):
186+
"""A deposit-shaped tx on a chain with NO pinned router must fall to
187+
the blind-sign gate — a router address borrowed onto an unpinned chain
188+
(where it may hold attacker code) cannot inherit the deposit UX."""
189+
self.requires_fullFeature()
190+
self.requires_firmware("7.15.0")
191+
self.setup_mnemonic_allallall()
192+
193+
from keepkeylib.client import CallException
194+
195+
memo = "=:ETH.ETH:0xabcdef1234567890abcdef1234567890abcdef12:0:t:0"
196+
data = _build_deposit_with_expiry_calldata(memo)
197+
198+
with self.assertRaises((CallException, Exception)):
199+
self.client.ethereum_sign_tx(
200+
n=parse_path("m/44'/60'/0'/0/0"),
201+
nonce=5,
202+
gas_price=50000000000,
203+
gas_limit=300000,
204+
to=binascii.unhexlify(THOR_ROUTER), # real mainnet router addr
205+
value=0,
206+
chain_id=56, # BSC: no pinned router
207+
data=data,
208+
)
209+
140210

141211
if __name__ == "__main__":
142212
unittest.main()

0 commit comments

Comments
 (0)