Summary
tools/abi.py: process_type mis-encodes multi-dimensional arrays (uint256[5][5] → uint256[55]), breaking clear-signing for Curve Router NG and any nested-array call.
The DB build tool flattens multi-dimensional array types when generating ABI entries. The 4-byte selector is stored correctly, but the argument-type metadata is wrong, so affected functions fail on-device argument validation and the device falls back to displaying raw calldata (i.e. blind signing).
This currently affects the entire Curve Router NG exchange family, which is present in the shipped database but stored with a corrupt argument schema.
Affected component
tools/abi.py → process_type, invoked via process_function → process_abi, driven by tools/shell-db.py. Present on master as of this writing.
Root cause
process_type parses the type string character by character, but it collects all bracketed dimensions into a single accumulator and wraps the base type in an array exactly once:
for c in abi_type["type"]:
if c.isdigit():
if type_in_array:
type_array_size = type_array_size + c # (1) never reset between brackets
else:
type_size = type_size + c
elif (c == "["):
type_in_array = True
elif (c == "]"):
type_in_array = False
type_array = True
...
if type_array: # (2) one wrapper, regardless of dimension count
arr_type = 0
if type_array_size == "":
arr_type = ETH_ABI_VARARRAY
else:
arr_type = (ETH_ABI_ARRAY | int(type_array_size))
res = {"type": arr_type, "child": [res]}
For uint256[5][5]:
(1) the digits of both [5] groups are concatenated → type_array_size = "55", and
(2) the base type is wrapped once → ETH_ABI_ARRAY | 55.
Net result: uint256[5][5] (a 5×5 nested array, 25 words) is encoded as uint256[55] (a single 55-element array, 55 words). Dynamic nesting (T[][]) collapses to a single T[], and mixed dimensions (T[3][], T[][3]) are likewise mis-parsed.
Note the selector is unaffected — it comes from eth_utils.abi_to_signature(func) in process_function, which produces the correct canonical signature. So affected entries match on selector but carry an incorrect argument schema.
Impact
In the shipped DB (version 20260409), the Curve Router NG exchange overloads are present but corrupt. Decoding the AB records shows all three storing uint256[55] where the real signature is uint256[5][5]:
| stored selector |
correct signature |
stored (corrupt) schema |
371dc447 |
exchange(address[11],uint256[5][5],uint256,uint256) |
…uint256[55]… |
5c9c18e2 |
exchange(address[11],uint256[5][5],uint256,uint256,address[5]) |
…uint256[55]… |
c872a3c5 |
exchange(address[11],uint256[5][5],uint256,uint256,address[5],address) |
…uint256[55]… |
On-device, a real Curve Router NG swap is therefore not recognized and shown as raw hex. The selector matches the stored entry, but eth_data_find_function (app/ethereum/eth_data.c) fails argument-structure validation against the corrupt schema, so eth_data_recognize returns NULL and the transaction is blind-signed.
Concrete example (mainnet), a crvUSD → USDC swap through exchange(address[11],uint256[5][5],uint256,uint256,address[5]) (selector 5c9c18e2): tx 0x7a2b64cc18f4853d0ca5b04e3cefcb0c6cfdb0024a17aa2b528ffd9f7c18431e.
This generalizes to any contract whose functions take multi-dimensional arrays; Curve Router NG is simply the most widely used case.
Reproduction
- Build the DB with any input containing a multi-dimensional-array function (or use the shipped
20260409 DB).
- Parse the resulting
AB records and reconstruct the stored signature from the argument-type tree — the nested dimension is collapsed (e.g. uint256[55] instead of uint256[5][5]).
- On a device, sign a Curve Router NG
exchange call and observe that it displays raw calldata rather than decoded arguments.
Proposed fix
Parse the dimensions into an ordered list and emit one array wrapper per dimension (innermost first; per Solidity/ABI, the rightmost bracket is the outermost array):
def process_type(abi_type):
type_base = ""
type_size = ""
dims = []
cur_dim = ""
type_in_array = False
for c in abi_type["type"]:
if c.isdigit():
if type_in_array:
cur_dim = cur_dim + c
else:
type_size = type_size + c
elif (c == "["):
type_in_array = True
cur_dim = ""
elif (c == "]"):
type_in_array = False
dims.append(int(cur_dim) if cur_dim != "" else None) # None => dynamic []
else:
type_base = type_base + c
res = {"child": []}
if type_base == "tuple":
res["type"] = ETH_ABI_TUPLE
for component in abi_type["components"]:
res["child"] = res["child"] + [process_type(component)]
elif type_base == "int":
res["type"] = (ETH_ABI_INT | bitsize(type_size))
elif type_base == "uint":
res["type"] = (ETH_ABI_UINT | bitsize(type_size))
elif (type_base == "fixed") or (type_base == "fixedx"):
res["type"] = (ETH_ABI_FIXED | 32)
elif (type_base == "ufixed") or (type_base == "ufixedx"):
res["type"] = (ETH_ABI_UFIXED | 32)
elif type_base == "address":
res["type"] = (ETH_ABI_ADDRESS | 20)
elif type_base == "bytes":
if type_size == "":
res["type"] = ETH_ABI_VARBYTES
else:
res["type"] = (ETH_ABI_BYTES | int(type_size))
elif type_base == "string":
res["type"] = ETH_ABI_STRING
elif type_base == "bool":
res["type"] = (ETH_ABI_BOOL | 1)
# one array wrapper per dimension; innermost first
for d in dims:
arr_type = ETH_ABI_VARARRAY if d is None else (ETH_ABI_ARRAY | d)
res = {"type": arr_type, "child": [res]}
return res
What changed: type_array/type_array_size become an ordered dims list (each [ resets cur_dim, each ] appends one dimension, None for dynamic), and the single wrapper becomes a loop that applies one wrapper per dimension.
Verified against the existing serialize_argument and a decoder round-trip on the 5-arg Router NG exchange:
before (current): exchange(address[11],uint256[55],uint256,uint256,address[5]) [73 words]
after (patched): exchange(address[11],uint256[5][5],uint256,uint256,address[5]) [43 words]
43 words matches the real calldata; 73 does not.
Please also verify the on-device decoder path
The packer fix produces a correct nested tree, but these signatures have never actually been exercised on-device (they are currently unreachable because of the corrupt schema). It would be worth an end-to-end test — recognition and rendering — for a function with static multi-dimensional arrays such as the Router NG exchange, to confirm the fix is sufficient on its own.
One spot worth checking specifically: in eth_data.c, eth_data_tuple_get_elem reads an offset word (content_off) for ETH_ABI_COMPOSITE arguments including the non-VARLEN (fixed-size) branch. Static fixed arrays are encoded inline in ABI calldata (no offset), so it's worth confirming the fixed-array head handling is correct once entries with valid nested schemas actually reach it.
Suggested regression test
A build-time round-trip check would have caught this: for each generated ABI entry, reconstruct the canonical signature from the serialized argument tree and assert that keccak(signature)[:4] equals the stored selector. Representative cases to cover: nested fixed arrays (uint256[5][5]), dynamic arrays (address[]), arrays of tuples, and mixed dimensions.
Summary
tools/abi.py:process_typemis-encodes multi-dimensional arrays (uint256[5][5]→uint256[55]), breaking clear-signing for Curve Router NG and any nested-array call.The DB build tool flattens multi-dimensional array types when generating ABI entries. The 4-byte selector is stored correctly, but the argument-type metadata is wrong, so affected functions fail on-device argument validation and the device falls back to displaying raw calldata (i.e. blind signing).
This currently affects the entire Curve Router NG
exchangefamily, which is present in the shipped database but stored with a corrupt argument schema.Affected component
tools/abi.py→process_type, invoked viaprocess_function→process_abi, driven bytools/shell-db.py. Present onmasteras of this writing.Root cause
process_typeparses the type string character by character, but it collects all bracketed dimensions into a single accumulator and wraps the base type in an array exactly once:For
uint256[5][5]:(1)the digits of both[5]groups are concatenated →type_array_size = "55", and(2)the base type is wrapped once →ETH_ABI_ARRAY | 55.Net result:
uint256[5][5](a 5×5 nested array, 25 words) is encoded asuint256[55](a single 55-element array, 55 words). Dynamic nesting (T[][]) collapses to a singleT[], and mixed dimensions (T[3][],T[][3]) are likewise mis-parsed.Note the selector is unaffected — it comes from
eth_utils.abi_to_signature(func)inprocess_function, which produces the correct canonical signature. So affected entries match on selector but carry an incorrect argument schema.Impact
In the shipped DB (version
20260409), the Curve Router NGexchangeoverloads are present but corrupt. Decoding theABrecords shows all three storinguint256[55]where the real signature isuint256[5][5]:371dc447exchange(address[11],uint256[5][5],uint256,uint256)…uint256[55]…5c9c18e2exchange(address[11],uint256[5][5],uint256,uint256,address[5])…uint256[55]…c872a3c5exchange(address[11],uint256[5][5],uint256,uint256,address[5],address)…uint256[55]…On-device, a real Curve Router NG swap is therefore not recognized and shown as raw hex. The selector matches the stored entry, but
eth_data_find_function(app/ethereum/eth_data.c) fails argument-structure validation against the corrupt schema, soeth_data_recognizereturnsNULLand the transaction is blind-signed.Concrete example (mainnet), a
crvUSD → USDCswap throughexchange(address[11],uint256[5][5],uint256,uint256,address[5])(selector5c9c18e2): tx0x7a2b64cc18f4853d0ca5b04e3cefcb0c6cfdb0024a17aa2b528ffd9f7c18431e.This generalizes to any contract whose functions take multi-dimensional arrays; Curve Router NG is simply the most widely used case.
Reproduction
20260409DB).ABrecords and reconstruct the stored signature from the argument-type tree — the nested dimension is collapsed (e.g.uint256[55]instead ofuint256[5][5]).exchangecall and observe that it displays raw calldata rather than decoded arguments.Proposed fix
Parse the dimensions into an ordered list and emit one array wrapper per dimension (innermost first; per Solidity/ABI, the rightmost bracket is the outermost array):
What changed:
type_array/type_array_sizebecome an ordereddimslist (each[resetscur_dim, each]appends one dimension,Nonefor dynamic), and the single wrapper becomes a loop that applies one wrapper per dimension.Verified against the existing
serialize_argumentand a decoder round-trip on the 5-arg Router NG exchange:43 words matches the real calldata; 73 does not.
Please also verify the on-device decoder path
The packer fix produces a correct nested tree, but these signatures have never actually been exercised on-device (they are currently unreachable because of the corrupt schema). It would be worth an end-to-end test — recognition and rendering — for a function with static multi-dimensional arrays such as the Router NG
exchange, to confirm the fix is sufficient on its own.One spot worth checking specifically: in
eth_data.c,eth_data_tuple_get_elemreads an offset word (content_off) forETH_ABI_COMPOSITEarguments including the non-VARLEN(fixed-size) branch. Static fixed arrays are encoded inline in ABI calldata (no offset), so it's worth confirming the fixed-array head handling is correct once entries with valid nested schemas actually reach it.Suggested regression test
A build-time round-trip check would have caught this: for each generated ABI entry, reconstruct the canonical signature from the serialized argument tree and assert that
keccak(signature)[:4]equals the stored selector. Representative cases to cover: nested fixed arrays (uint256[5][5]), dynamic arrays (address[]), arrays of tuples, and mixed dimensions.