Skip to content

Commit 2a430da

Browse files
committed
fix(ethereum): require AdvancedMode for transformERC20
1 parent 57d6024 commit 2a430da

2 files changed

Lines changed: 30 additions & 81 deletions

File tree

lib/firmware/ethereum_contracts.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
#include "keepkey/firmware/ethereum_contracts/thortx.h"
2626
#include "keepkey/firmware/ethereum_contracts/zxappliquid.h"
2727
#include "keepkey/firmware/ethereum_contracts/zxliquidtx.h"
28-
#include "keepkey/firmware/ethereum_contracts/zxtransERC20.h"
2928
#include "keepkey/firmware/ethereum_contracts/zxswap.h"
3029
#include "keepkey/firmware/ethereum_contracts/makerdao.h"
3130

@@ -75,11 +74,6 @@ bool ethereum_contractHandled(uint32_t data_total, const EthereumSignTx* msg,
7574
* guarantees the minimum, so establish it once here. */
7675
if (msg->data_initial_chunk.size < 4) return false;
7776

78-
/* 0x transformERC20 is pinned to the ExchangeProxy address and its outcome
79-
* is bounded by the input amount and minimum output amount shown on screen,
80-
* so it stays clear-signable at any calldata size that fits one chunk. */
81-
if (zx_isZxTransformERC20(msg)) return true;
82-
8377
if (sa_isWithdrawFromSalary(msg)) return true;
8478
if (zx_isZxSwap(msg)) return true;
8579
if (zx_isZxLiquidTx(msg)) return true;
@@ -103,9 +97,6 @@ bool ethereum_contractConfirmed(uint32_t data_total, const EthereumSignTx* msg,
10397
if (sa_isWithdrawFromSalary(msg))
10498
return sa_confirmWithdrawFromSalary(data_total, msg);
10599

106-
if (zx_isZxTransformERC20(msg))
107-
return zx_confirmZxTransERC20(data_total, msg);
108-
109100
if (zx_isZxSwap(msg)) return zx_confirmZxSwap(data_total, msg);
110101

111102
if (zx_isZxLiquidTx(msg)) return zx_confirmZxLiquidTx(data_total, msg, node);

unittests/firmware/ethereum.cpp

Lines changed: 30 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -279,91 +279,49 @@ TEST(Ethereum, ThorchainNativeAssetUsesOnlyItsZeroAddressSentinel) {
279279
EXPECT_FALSE(thor_assetIsNative(nullptr));
280280
}
281281

282-
// transformERC20(address,address,uint256,uint256,(uint32,bytes)[]) — the two
283-
// address words carry the token in their low 20 bytes.
284-
static void MakeTransformErc20(EthereumSignTx* msg, const char* in_token,
285-
const char* out_token) {
282+
// A canonical transformERC20 call with one transformation whose data is one
283+
// byte. The transformation byte is deliberately outside the four static words
284+
// that the retired decoder displayed.
285+
static void MakeTransformErc20(EthereumSignTx* msg, uint8_t transform_byte) {
286286
*msg = EthereumSignTx{};
287287
msg->has_to = true;
288288
msg->to.size = 20;
289289
std::memcpy(msg->to.bytes, ZXSWAP_ADDRESS, msg->to.size);
290290
msg->has_chain_id = true;
291291
msg->chain_id = 1;
292292
msg->has_data_initial_chunk = true;
293-
msg->data_initial_chunk.size = 4 + 4 * 32;
293+
msg->data_initial_chunk.size = 4 + 11 * 32;
294294
std::memcpy(msg->data_initial_chunk.bytes, "\x41\x55\x65\xb0", 4);
295-
if (in_token)
296-
std::memcpy(msg->data_initial_chunk.bytes + 4 + 12, in_token, 20);
297-
if (out_token)
298-
std::memcpy(msg->data_initial_chunk.bytes + 4 + 32 + 12, out_token, 20);
295+
std::memcpy(msg->data_initial_chunk.bytes + 4 + 12, kTUSD, 20);
296+
std::memcpy(msg->data_initial_chunk.bytes + 4 + 32 + 12, kTGBP, 20);
297+
msg->data_initial_chunk.bytes[4 + 3 * 32 - 1] = 1; // input amount
298+
msg->data_initial_chunk.bytes[4 + 4 * 32 - 1] = 1; // minimum output
299+
msg->data_initial_chunk.bytes[4 + 5 * 32 - 1] = 0xa0; // array offset
300+
msg->data_initial_chunk.bytes[4 + 6 * 32 - 1] = 1; // array length
301+
msg->data_initial_chunk.bytes[4 + 7 * 32 - 1] = 0x20; // element offset
302+
msg->data_initial_chunk.bytes[4 + 8 * 32 - 1] = 1; // deployment nonce
303+
msg->data_initial_chunk.bytes[4 + 9 * 32 - 1] = 0x40; // data offset
304+
msg->data_initial_chunk.bytes[4 + 10 * 32 - 1] = 1; // data length
305+
msg->data_initial_chunk.bytes[4 + 10 * 32] = transform_byte;
299306
}
300307

301-
TEST(Ethereum, TransformErc20RequiresCompleteCalldataForClearSigning) {
302-
EthereumSignTx msg;
303-
MakeTransformErc20(&msg, kTUSD, kTGBP);
308+
TEST(Ethereum, TransformErc20AlwaysRequiresAdvancedMode) {
309+
EthereumSignTx first, second;
310+
MakeTransformErc20(&first, 0x41);
311+
MakeTransformErc20(&second, 0x42);
304312

305-
EXPECT_TRUE(
306-
ethereum_contractHandled(msg.data_initial_chunk.size, &msg, nullptr));
307-
EXPECT_FALSE(
308-
ethereum_contractHandled(msg.data_initial_chunk.size + 1, &msg, nullptr));
309-
}
310-
311-
TEST(Ethereum, TransformErc20RejectsAmountsThatDoNotFitTheDisplay) {
312-
EthereumSignTx msg;
313-
MakeTransformErc20(&msg, kTUSD, kTGBP);
314-
315-
std::memset(msg.data_initial_chunk.bytes + 4 + 2 * 32, 0xff, 32);
316-
EXPECT_FALSE(zx_confirmZxTransERC20(msg.data_initial_chunk.size, &msg));
317-
318-
MakeTransformErc20(&msg, kTUSD, kTGBP);
319-
std::memset(msg.data_initial_chunk.bytes + 4 + 3 * 32, 0xff, 32);
320-
EXPECT_FALSE(zx_confirmZxTransERC20(msg.data_initial_chunk.size, &msg));
321-
}
313+
ASSERT_EQ(first.data_initial_chunk.size, second.data_initial_chunk.size);
314+
ASSERT_EQ(0, std::memcmp(first.data_initial_chunk.bytes,
315+
second.data_initial_chunk.bytes,
316+
first.data_initial_chunk.size - 32));
317+
ASSERT_NE(0, std::memcmp(first.data_initial_chunk.bytes,
318+
second.data_initial_chunk.bytes,
319+
first.data_initial_chunk.size));
322320

323-
// The decoder shows four values and hides the transformations[] body. That is
324-
// only defensible because the input amount and minimum output amount bound the
325-
// outcome — and ethereumFormatAmount() renders the literal "Unknown token
326-
// value" whenever tokenByChainAddress() misses, so an unresolved token turns
327-
// the bound into nothing while the calldata still executes.
328-
//
329-
// Gating on the lookup rather than on a chain allowlist keeps this correct
330-
// however the tables change. It matters in practice: the generated table
331-
// carries ~1924 entries for chain 1, three each for BSC and Polygon, and NONE
332-
// for Base, Arbitrum or Avalanche, so on those chains every pair fails here.
333-
TEST(Ethereum, TransformErc20RequiresBothTokensResolvable) {
334-
EthereumSignTx msg;
335-
336-
// Both known -> the device can name what it is showing.
337-
MakeTransformErc20(&msg, kTUSD, kTGBP);
338-
EXPECT_TRUE(
339-
ethereum_contractHandled(msg.data_initial_chunk.size, &msg, nullptr));
340-
341-
// Either side unknown -> refuse to claim it, so ethereum.c falls through to
342-
// the raw-calldata path (AdvancedMode-gated, bytes shown).
343-
MakeTransformErc20(&msg, nullptr, kTGBP);
344-
EXPECT_FALSE(
345-
ethereum_contractHandled(msg.data_initial_chunk.size, &msg, nullptr))
346-
<< "unknown INPUT token must not clear-sign";
347-
348-
MakeTransformErc20(&msg, kTUSD, nullptr);
349321
EXPECT_FALSE(
350-
ethereum_contractHandled(msg.data_initial_chunk.size, &msg, nullptr))
351-
<< "unknown OUTPUT token must not clear-sign";
352-
353-
MakeTransformErc20(&msg, nullptr, nullptr);
354-
EXPECT_FALSE(
355-
ethereum_contractHandled(msg.data_initial_chunk.size, &msg, nullptr));
356-
357-
// A chain with no token table entries at all cannot name either asset, so it
358-
// must refuse even though 0x deploys the same proxy there. This is what the
359-
// chain allowlist was previously being asked to approximate.
360-
for (uint32_t cid : {8453u, 42161u, 43114u}) {
361-
MakeTransformErc20(&msg, kTUSD, kTGBP);
362-
msg.chain_id = cid;
363-
EXPECT_FALSE(
364-
ethereum_contractHandled(msg.data_initial_chunk.size, &msg, nullptr))
365-
<< "chain " << cid << " has no token entries; nothing is nameable";
366-
}
322+
ethereum_contractHandled(first.data_initial_chunk.size, &first, nullptr));
323+
EXPECT_FALSE(ethereum_contractHandled(second.data_initial_chunk.size, &second,
324+
nullptr));
367325
}
368326

369327
TEST(Ethereum, Eip712ChainIdRequiresCanonicalUint32) {

0 commit comments

Comments
 (0)