feat(core/ethereum): ERC-20 smart contracts

- preparatory work - WIP
obrusvit/erc20-func-support
obrusvit 9 months ago
parent 1a9bb11806
commit 128f47b3ee

@ -5,7 +5,7 @@
},
"tests": [
{
"name": "erc20_token",
"name": "erc20_transfer",
"parameters": {
"comment": "Sending 291 Grzegorz Brzęczyszczykiewicz tokens to address 0x574bbb36871ba6b78e27f4b4dcfb76ea0091880b",
"data": "a9059cbb000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000123",
@ -24,6 +24,26 @@
"sig_s": "633a74429eb6d3aeec4ed797542236a85daab3cab15e37736b87a45697541d7a"
}
},
{
"name": "erc20_approve",
"parameters": {
"comment": "Approving spending of 100 Grzegorz Brzęczyszczykiewicz tokens for address 0x574bbb36871ba6b78e27f4b4dcfb76ea0091880b",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000064",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_price": "0x14",
"gas_limit": "0x14",
"tx_type": null,
"value": "0x0"
},
"result": {
"sig_v": 37,
"sig_r": "b10bb751b48d72bbb6d55ecb34800c08f92fda818480f67190d923c5bd73d2c4",
"sig_s": "78fb7fa237bc6bfa0e5bdb96164db4162f41866f2f30b54295d947fe87a813a8"
}
},
{
"name": "wanchain",
"parameters": {

@ -62,7 +62,7 @@
}
},
{
"name": "erc20",
"name": "erc20_transfer",
"parameters": {
"comment": "Sending 291 Grzegorz Brzęczyszczykiewicz tokens to address 0x574bbb36871ba6b78e27f4b4dcfb76ea0091880b",
"data": "a9059cbb000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000123",
@ -81,6 +81,26 @@
"sig_s": "399bff8752539176c4b2f1d5d2a8f6029f79841d28802149ab339a033ffe4c1f"
}
},
{
"name": "erc20_approve",
"parameters": {
"comment": "Approving spending of 100 Grzegorz Brzęczyszczykiewicz tokens for address 0x574bbb36871ba6b78e27f4b4dcfb76ea0091880b",
"data": "095ea7b3000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000064",
"path": "m/44'/60'/0'/0/0",
"to_address": "0xfc6b5d6af8a13258f7cbd0d39e11b35e01a32f93",
"chain_id": 1,
"nonce": "0x0",
"gas_limit": "0x14",
"max_gas_fee": "0x14",
"max_priority_fee": "0x1",
"value": "0x0"
},
"result": {
"sig_v": 0,
"sig_r": "e172298f9e5aeef170bfd9acff39249de3c7abbdc5637c135b2e82247e8ee12a",
"sig_s": "129cd1e0deaf4bd82e1064409188f1fadc8b7e6adfeb04ae4e407d5451ff43d8"
}
},
{
"name": "large_chainid",
"parameters": {

@ -23,6 +23,17 @@ if TYPE_CHECKING:
)
async def require_confirm_smart_contract(
func_name: str | bytes, func_args: list[tuple[str, str | bytes]]
):
from trezor.ui.layouts import confirm_properties
await confirm_properties(
"confirm_smart_contract", "Smart contract", [("Function:", func_name)]
)
await confirm_properties("confirm_smart_contract", "Smart contract", func_args)
async def require_confirm_tx(
to_bytes: bytes,
value: int,

@ -14,6 +14,7 @@ if TYPE_CHECKING:
from .definitions import Definitions
from .keychain import MsgInSignTx
from typing import Any
# Maximum chain_id which returns the full signature_v (which must fit into an uint32).
@ -32,7 +33,6 @@ async def sign_tx(
from trezor.utils import HashWriter
from apps.common import paths
from .layout import require_confirm_data, require_confirm_tx
# check
@ -44,16 +44,11 @@ async def sign_tx(
await paths.validate_path(keychain, msg.address_n)
# Handle ERC20s
token, address_bytes, recipient, value = await handle_erc20(msg, defs)
data_total = msg.data_length # local_cache_attribute
if token is None and data_total > 0:
await require_confirm_data(msg.data_initial_chunk, data_total)
address_bytes = bytes_from_address(msg.to)
token, value = await sign_tx_common(msg, defs, address_bytes)
await require_confirm_tx(
recipient,
address_bytes,
value,
int.from_bytes(msg.gas_price, "big"),
int.from_bytes(msg.gas_limit, "big"),
@ -62,6 +57,7 @@ async def sign_tx(
bool(msg.chunkify),
)
data_total = msg.data_length
data = bytearray()
data += msg.data_initial_chunk
data_left = data_total - len(msg.data_initial_chunk)
@ -99,33 +95,136 @@ async def sign_tx(
return result
async def handle_erc20(
async def sign_tx_common(
msg: MsgInSignTx,
definitions: Definitions,
) -> tuple[EthereumTokenInfo | None, bytes, bytes, int]:
address_bytes: bytes,
) -> tuple[EthereumTokenInfo | None, int]:
from .layout import (
require_confirm_unknown_token,
require_confirm_smart_contract,
require_confirm_tx,
require_confirm_data,
)
from . import tokens
from .layout import require_confirm_unknown_token
data_initial_chunk = msg.data_initial_chunk # local_cache_attribute
token = None
address_bytes = recipient = bytes_from_address(msg.to)
value = int.from_bytes(msg.value, "big")
if (
len(msg.to) in (40, 42)
and len(msg.value) == 0
and msg.data_length == 68
and len(data_initial_chunk) == 68
and data_initial_chunk[:16]
== b"\xa9\x05\x9c\xbb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
):
if len(msg.to) in (40, 42) and value == 0:
# Smart Contract
func_name, func_args, transfer = _resolve_tx_data_field(data_initial_chunk)
# TODO handle ValueError from _resolve_tx_data_field, presently it fails `test_data_streaming`
token = definitions.get_token(address_bytes)
recipient = data_initial_chunk[16:36]
value = int.from_bytes(data_initial_chunk[36:68], "big")
if token is tokens.UNKNOWN_TOKEN:
await require_confirm_unknown_token(address_bytes)
return token, address_bytes, recipient, value
if transfer[0]:
# 'transfer' functions should override the value
arg_val_idx = transfer[1]
value = int(func_args[arg_val_idx][1])
else:
# we want to show default network at summary screen (i.e. 0 ETH)
token = None
await require_confirm_smart_contract(func_name, func_args)
else:
# Regular transaction
await require_confirm_tx(address_bytes, value, definitions.network, token)
if msg.data_length > 0:
await require_confirm_data(data_initial_chunk, msg.data_length)
return token, value
def _resolve_type(val: memoryview, type_str: str) -> str:
from ubinascii import hexlify
from .helpers import address_from_bytes
if type_str == "int":
return str(int.from_bytes(val, "big"))
elif type_str == "str":
# TODO improve shown text
return bytes(val).decode()
elif type_str == "bytes":
return hexlify(val).decode()
elif type_str == "address":
return address_from_bytes(val[-20:])
else:
raise ValueError
FUNCTIONS_DEF: dict[bytes, dict[str, Any]] = {
b"\xa9\x05\x9c\xbb": {
"name": "transfer",
"args": [
("Recipient", "address"),
("Amount", "int"),
],
"transfer": (True, 1),
},
b"\x09\x5e\xa7\xb3": {
"name": "approve",
"args": [
("Address", "address"),
("Amount", "int"),
],
"transfer": (False, 0),
},
b"\x00\x00\x00\x42": {
"name": "args_test",
"args": [
("Arg0_int", "int"),
("Arg1_str", "str"),
("Arg2_bytes", "bytes"),
("Arg3_address", "address"),
],
"transfer": (False, 0),
# TODO token to address assignment is done by additional entry, where:
# - 1st value is the idx of the value of a token
# - 2nd value is the idx of the address of the token -> to be used in the `definitions.get_token(addr)` call
# - if 1st == 2nd: token is assigned based on contract address
# "token_assign": (0, 3),
},
}
def _resolve_tx_data_field(
data_bytes: bytes,
) -> tuple[str, list[tuple[str, str | bytes]], tuple[bool, int]]:
from ubinascii import hexlify
data_args_len = len(data_bytes)
N_BYTES_FUNC = 4
N_BYTES_ARG = 32
n_args = (data_args_len - N_BYTES_FUNC) // N_BYTES_ARG
data = memoryview(data_bytes)
def _data_field_aligned(data_args_len: int, n_args: int) -> bool:
# checks if "Data" field doesn't have trailing bytes
return data_args_len == (n_args * N_BYTES_ARG + N_BYTES_FUNC)
def _get_nth_arg(data_mv: memoryview, n: int) -> memoryview:
# returns slice of the nth argument in "Data" field
beg = (n + 0) * N_BYTES_ARG + N_BYTES_FUNC
end = (n + 1) * N_BYTES_ARG + N_BYTES_FUNC
return data_mv[beg:end]
if data_args_len < N_BYTES_FUNC or not _data_field_aligned(data_args_len, n_args):
raise ValueError
func_signature_bytes = data_bytes[:N_BYTES_FUNC]
func_def = FUNCTIONS_DEF.get(func_signature_bytes, None)
if func_def is not None and n_args == len(func_def["args"]):
func_name = func_def["name"]
func_args = [
(f"{name}:", _resolve_type(_get_nth_arg(data, i), type_str))
for i, (name, type_str) in enumerate(func_def["args"])
]
transfer = func_def["transfer"]
else:
func_name = hexlify(func_signature_bytes).decode()
func_args = [(f"Input {i}:", _get_nth_arg(data, i)) for i in range(n_args)]
transfer = (False, 0)
return (func_name, func_args, transfer)
def _get_total_length(msg: EthereumSignTx, data_total: int) -> int:

@ -41,9 +41,8 @@ async def sign_tx_eip1559(
from trezor.utils import HashWriter
from apps.common import paths
from .layout import require_confirm_data, require_confirm_tx_eip1559
from .sign_tx import check_common_fields, handle_erc20, send_request_chunk
from .layout import require_confirm_tx_eip1559
from .sign_tx import sign_tx_common, send_request_chunk, check_common_fields
gas_limit = msg.gas_limit # local_cache_attribute
data_total = msg.data_length # local_cache_attribute
@ -57,11 +56,8 @@ async def sign_tx_eip1559(
await paths.validate_path(keychain, msg.address_n)
# Handle ERC20s
token, address_bytes, recipient, value = await handle_erc20(msg, defs)
if token is None and data_total > 0:
await require_confirm_data(msg.data_initial_chunk, data_total)
address_bytes = bytes_from_address(msg.to)
token, value = await sign_tx_common(msg, defs, address_bytes)
await require_confirm_tx_eip1559(
recipient,
@ -74,6 +70,7 @@ async def sign_tx_eip1559(
bool(msg.chunkify),
)
data_total = msg.data_length
data = bytearray()
data += msg.data_initial_chunk
data_left = data_total - len(msg.data_initial_chunk)

@ -903,7 +903,7 @@ async def confirm_properties(
from ubinascii import hexlify
def handle_bytes(prop: PropertyType):
if isinstance(prop[1], bytes):
if isinstance(prop[1], (bytes, bytearray, memoryview)):
return (prop[0], hexlify(prop[1]).decode(), True)
else:
# When there is not space in the text, taking it as data

@ -890,7 +890,10 @@ async def confirm_properties(
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
) -> None:
# Monospace flag for values that are bytes.
items = [(prop[0], prop[1], isinstance(prop[1], bytes)) for prop in props]
items = [
(prop[0], prop[1], isinstance(prop[1], (bytes, bytearray, memoryview)))
for prop in props
]
await raise_if_not_confirmed(
interact(

@ -158,6 +158,139 @@ def test_signtx_eip1559(client: Client, chunkify: bool, parameters: dict, result
assert sig_v == result["sig_v"]
def test_signtx_erc20_balanceof_misaligned_data(client: Client):
# "Data" field for a 'balanceOf' call with two last bytes removed.
with pytest.raises(TrezorFailure):
with client:
ethereum.sign_tx_eip1559(
client,
n=parse_path("m/44h/60h/0h/0/0"),
nonce=0,
gas_limit=20,
max_gas_fee=20,
max_priority_fee=1,
to=TO_ADDR,
chain_id=1,
value=0,
data=bytes.fromhex(
"70a08231000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea009188"
),
)
def test_signtx_erc20_balanceof(client: Client):
# "Data" field for a 'balanceOf' call. The function checks the balance of the address 0x574bbb36871ba6b78e27f4b4dcfb76ea0091880b
# The function has 1 argument: 'address'
with client:
ethereum.sign_tx_eip1559(
client,
n=parse_path("m/44h/60h/0h/0/0"),
nonce=0,
gas_limit=20,
max_gas_fee=20,
max_priority_fee=1,
to=TO_ADDR,
chain_id=1,
value=0,
data=bytes.fromhex(
"70a08231000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b"
),
)
def test_signtx_erc20_allowance(client: Client):
# "Data" field for an 'allowance' call. This function checks the amount of tokens that an owner allowed to a spender.
# The function has 2 arguments: 'address', 'address'
with client:
ethereum.sign_tx_eip1559(
client,
n=parse_path("m/44h/60h/0h/0/0"),
nonce=0,
gas_limit=20,
max_gas_fee=20,
max_priority_fee=1,
to=TO_ADDR,
chain_id=1,
value=0,
data=bytes.fromhex(
"dd62ed3e0000000000000000000000001111111111111111111111111111111111111111000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b"
),
)
def test_signtx_erc20_transferfrom(client: Client):
# "Data" field for a 'transferFrom' call. The function allows a spender to transfer a certain amount of tokens from the owner's balance to another address.
# The function has 3 arguments: 'from', 'to', 'value'
with client:
ethereum.sign_tx_eip1559(
client,
n=parse_path("m/44h/60h/0h/0/0"),
nonce=0,
gas_limit=20,
max_gas_fee=20,
max_priority_fee=1,
to=TO_ADDR,
chain_id=1,
value=0,
data=bytes.fromhex(
"23b872dd0000000000000000000000001111111111111111111111111111111111111111000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b0000000000000000000000000000000000000000000000000000000000000064"
),
)
def test_signtx_erc20_invity(client: Client):
# https://eth1.trezor.io/tx/0xcce715f7bd2fb509a41f64519d29e5d666e5f2f664ea18ce486d446e83466e56
# "nonce": "0x10",
# "gasPrice": "0x737be7600",
# "gas": "0x39805",
# "to": "0x1111111254EEB25477B68fb85Ed929f73A960582",
# "value": "0x26b81237cb570000",
# "input": "0x12aa3caf00000000000000000000000092f3f71cef740ed5784874b8c70ff87ecdf33588000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000092f3f71cef740ed5784874b8c70ff87ecdf335880000000000000000000000007490ca03e52adbd7a05ed8cc30a228aeb968ae2500000000000000000000000000000000000000000000000026b81237cb5700000000000000000000000000000000000000000000000000000000000128cd0eef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e10000000000000000000000000000000000c300009500004600002c000026e021973ee919693206e122950ff4b162ac52eb6248db00000000000000000080db5f7200e00000206b4be0b94041c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0e30db002a00000000000000000000000000000000000000000000000000000000128cd0eefee63c1e50088e6a0c2ddd26feeb64f039a2c41296fcb3f5640c02aaa39b223fe8d0a0e5c4f27ead9083c756cc280a06c4eca27a0b86991c6218b36c1d19d4a2e9eb0ce3606eb481111111254eeb25477b68fb85ed929f73a96058200000000000000000000000000000000000000000000000000000000000000cb1c12d1",
# "hash": "0xcce715f7bd2fb509a41f64519d29e5d666e5f2f664ea18ce486d446e83466e56",
# "blockNumber": "0x10fed3b",
# "from": "0x7490CA03E52ADbD7A05ed8cc30a228AeB968ae25",
# "transactionIndex": "0x72"
with client:
ethereum.sign_tx(
client,
n=parse_path("m/44h/60h/0h/0/0"),
nonce=0x10,
gas_price=0x737BE7600,
gas_limit=0x39805,
to="0x1111111254EEB25477B68fb85Ed929f73A960582",
chain_id=1,
value=0x26B81237CB570000, # TODO this wouldn't pass the check for smart contract
tx_type=None,
data=bytes.fromhex(
"12aa3caf00000000000000000000000092f3f71cef740ed5784874b8c70ff87ecdf33588000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000092f3f71cef740ed5784874b8c70ff87ecdf335880000000000000000000000007490ca03e52adbd7a05ed8cc30a228aeb968ae2500000000000000000000000000000000000000000000000026b81237cb5700000000000000000000000000000000000000000000000000000000000128cd0eef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e10000000000000000000000000000000000c300009500004600002c000026e021973ee919693206e122950ff4b162ac52eb6248db00000000000000000080db5f7200e00000206b4be0b94041c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0e30db002a00000000000000000000000000000000000000000000000000000000128cd0eefee63c1e50088e6a0c2ddd26feeb64f039a2c41296fcb3f5640c02aaa39b223fe8d0a0e5c4f27ead9083c756cc280a06c4eca27a0b86991c6218b36c1d19d4a2e9eb0ce3606eb481111111254eeb25477b68fb85ed929f73a96058200000000000000000000000000000000000000000000000000000000000000cb1c12d1"
),
)
def test_signtx_erc20_args_test(client: Client):
# Testing various data types in ERC-20 function arguments
arg0_int = "0000000000000000000000000000000000000000000000000000000000001251"
arg1_str = "00000000000000000000000000000000000000000000000000000048656c6c6f"
arg2_bytes = "1111111122222222333333334444444455555555666666667777777788888888"
arg3_address = "000000000000000000000000574bbb36871ba6b78e27f4b4dcfb76ea0091880b"
with client:
ethereum.sign_tx_eip1559(
client,
n=parse_path("m/44h/60h/0h/0/0"),
nonce=0,
gas_limit=20,
max_gas_fee=20,
max_priority_fee=1,
to=TO_ADDR,
chain_id=1,
value=0,
data=bytes.fromhex(
"00000042" + arg0_int + arg1_str + arg2_bytes + arg3_address
),
)
def test_sanity_checks(client: Client):
"""Is not vectorized because these are internal-only tests that do not
need to be exposed to the public.

Loading…
Cancel
Save