Improved NFT tests

Now uses the ERC 721/1155 ABIs, gets selector automatically, and actually verifies the signature
This commit is contained in:
Alexandre Paillier
2023-11-20 14:35:47 +01:00
parent ceb1cfaf4b
commit b90d660a69
3 changed files with 633 additions and 127 deletions

View File

@@ -0,0 +1,276 @@
[
{
"anonymous" : false,
"inputs" : [
{
"indexed" : true,
"internalType" : "address",
"name" : "_owner",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_operator",
"type" : "address"
},
{
"indexed" : false,
"internalType" : "bool",
"name" : "_approved",
"type" : "bool"
}
],
"name" : "ApprovalForAll",
"type" : "event"
},
{
"anonymous" : false,
"inputs" : [
{
"indexed" : true,
"internalType" : "address",
"name" : "_operator",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"indexed" : false,
"internalType" : "uint256[]",
"name" : "_ids",
"type" : "uint256[]"
},
{
"indexed" : false,
"internalType" : "uint256[]",
"name" : "_values",
"type" : "uint256[]"
}
],
"name" : "TransferBatch",
"type" : "event"
},
{
"anonymous" : false,
"inputs" : [
{
"indexed" : true,
"internalType" : "address",
"name" : "_operator",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"indexed" : false,
"internalType" : "uint256",
"name" : "_id",
"type" : "uint256"
},
{
"indexed" : false,
"internalType" : "uint256",
"name" : "_value",
"type" : "uint256"
}
],
"name" : "TransferSingle",
"type" : "event"
},
{
"anonymous" : false,
"inputs" : [
{
"indexed" : false,
"internalType" : "string",
"name" : "_value",
"type" : "string"
},
{
"indexed" : true,
"internalType" : "uint256",
"name" : "_id",
"type" : "uint256"
}
],
"name" : "URI",
"type" : "event"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_owner",
"type" : "address"
},
{
"internalType" : "uint256",
"name" : "_id",
"type" : "uint256"
}
],
"name" : "balanceOf",
"outputs" : [
{
"internalType" : "uint256",
"name" : "",
"type" : "uint256"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address[]",
"name" : "_owners",
"type" : "address[]"
},
{
"internalType" : "uint256[]",
"name" : "_ids",
"type" : "uint256[]"
}
],
"name" : "balanceOfBatch",
"outputs" : [
{
"internalType" : "uint256[]",
"name" : "",
"type" : "uint256[]"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_owner",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_operator",
"type" : "address"
}
],
"name" : "isApprovedForAll",
"outputs" : [
{
"internalType" : "bool",
"name" : "",
"type" : "bool"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"internalType" : "uint256[]",
"name" : "_ids",
"type" : "uint256[]"
},
{
"internalType" : "uint256[]",
"name" : "_values",
"type" : "uint256[]"
},
{
"internalType" : "bytes",
"name" : "_data",
"type" : "bytes"
}
],
"name" : "safeBatchTransferFrom",
"outputs" : [],
"stateMutability" : "nonpayable",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"internalType" : "uint256",
"name" : "_id",
"type" : "uint256"
},
{
"internalType" : "uint256",
"name" : "_value",
"type" : "uint256"
},
{
"internalType" : "bytes",
"name" : "_data",
"type" : "bytes"
}
],
"name" : "safeTransferFrom",
"outputs" : [],
"stateMutability" : "nonpayable",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_operator",
"type" : "address"
},
{
"internalType" : "bool",
"name" : "_approved",
"type" : "bool"
}
],
"name" : "setApprovalForAll",
"outputs" : [],
"stateMutability" : "nonpayable",
"type" : "function"
}
]

View File

@@ -0,0 +1,268 @@
[
{
"anonymous" : false,
"inputs" : [
{
"indexed" : true,
"internalType" : "address",
"name" : "_owner",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_approved",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "Approval",
"type" : "event"
},
{
"anonymous" : false,
"inputs" : [
{
"indexed" : true,
"internalType" : "address",
"name" : "_owner",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_operator",
"type" : "address"
},
{
"indexed" : false,
"internalType" : "bool",
"name" : "_approved",
"type" : "bool"
}
],
"name" : "ApprovalForAll",
"type" : "event"
},
{
"anonymous" : false,
"inputs" : [
{
"indexed" : true,
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"indexed" : true,
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "Transfer",
"type" : "event"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_approved",
"type" : "address"
},
{
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "approve",
"outputs" : [],
"stateMutability" : "payable",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_owner",
"type" : "address"
}
],
"name" : "balanceOf",
"outputs" : [
{
"internalType" : "uint256",
"name" : "",
"type" : "uint256"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "getApproved",
"outputs" : [
{
"internalType" : "address",
"name" : "",
"type" : "address"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_owner",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_operator",
"type" : "address"
}
],
"name" : "isApprovedForAll",
"outputs" : [
{
"internalType" : "bool",
"name" : "",
"type" : "bool"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "ownerOf",
"outputs" : [
{
"internalType" : "address",
"name" : "",
"type" : "address"
}
],
"stateMutability" : "view",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "safeTransferFrom",
"outputs" : [],
"stateMutability" : "payable",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
},
{
"internalType" : "bytes",
"name" : "data",
"type" : "bytes"
}
],
"name" : "safeTransferFrom",
"outputs" : [],
"stateMutability" : "payable",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_operator",
"type" : "address"
},
{
"internalType" : "bool",
"name" : "_approved",
"type" : "bool"
}
],
"name" : "setApprovalForAll",
"outputs" : [],
"stateMutability" : "nonpayable",
"type" : "function"
},
{
"inputs" : [
{
"internalType" : "address",
"name" : "_from",
"type" : "address"
},
{
"internalType" : "address",
"name" : "_to",
"type" : "address"
},
{
"internalType" : "uint256",
"name" : "_tokenId",
"type" : "uint256"
}
],
"name" : "transferFrom",
"outputs" : [],
"stateMutability" : "payable",
"type" : "function"
}
]

View File

@@ -1,17 +1,21 @@
import pytest import pytest
from typing import Optional, Any
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from ragger.error import ExceptionRAPDU from ragger.error import ExceptionRAPDU
from ragger.firmware import Firmware from ragger.firmware import Firmware
from ragger.backend import BackendInterface from ragger.backend import BackendInterface
from ragger.navigator import Navigator, NavInsID from ragger.navigator import Navigator, NavInsID
from ledger_app_clients.ethereum.client import EthAppClient, TxData, StatusWord from ledger_app_clients.ethereum.client import EthAppClient, StatusWord
from ledger_app_clients.ethereum.settings import SettingID, settings_toggle import ledger_app_clients.ethereum.response_parser as ResponseParser
from eth_utils import function_signature_to_4byte_selector from ledger_app_clients.ethereum.utils import get_selector_from_data, recover_transaction
import struct from web3 import Web3
import json
import os
ROOT_SCREENSHOT_PATH = Path(__file__).parent ROOT_SCREENSHOT_PATH = Path(__file__).parent
ABIS_FOLDER = "%s/abis" % (os.path.dirname(__file__))
BIP32_PATH = "m/44'/60'/0'/0/0" BIP32_PATH = "m/44'/60'/0'/0/0"
NONCE = 21 NONCE = 21
@@ -21,23 +25,25 @@ FROM = bytes.fromhex("1122334455667788990011223344556677889900")
TO = bytes.fromhex("0099887766554433221100998877665544332211") TO = bytes.fromhex("0099887766554433221100998877665544332211")
NFTS = [ (1, 3), (5, 2), (7, 4) ] # tuples of (token_id, amount) NFTS = [ (1, 3), (5, 2), (7, 4) ] # tuples of (token_id, amount)
DATA = "Some data".encode() DATA = "Some data".encode()
DEVICE_ADDR: Optional[bytes] = None
class NFTCollection: class NFTCollection:
addr: bytes addr: bytes
name: str name: str
chain_id: int chain_id: int
def __init__(self, addr: bytes, name: str, chain_id: int): def __init__(self, addr: bytes, name: str, chain_id: int, contract):
self.addr = addr self.addr = addr
self.name = name self.name = name
self.chain_id = chain_id self.chain_id = chain_id
self.contract = contract
class Action: class Action:
fn: str fn_name: str
data_fn: Callable fn_args: list[Any]
nav_fn: Callable nav_fn: Callable
def __init__(self, fn: str, data_fn: Callable, nav_fn: Callable): def __init__(self, fn_name: str, fn_args: list[Any], nav_fn: Callable):
self.fn = fn self.fn_name = fn_name
self.data_fn = data_fn self.fn_args = fn_args
self.nav_fn = nav_fn self.nav_fn = nav_fn
def common_nav_nft(is_nano: bool, nano_steps: int, stax_steps: int, reject: bool) -> list[NavInsID]: def common_nav_nft(is_nano: bool, nano_steps: int, stax_steps: int, reject: bool) -> list[NavInsID]:
@@ -59,7 +65,7 @@ def common_nav_nft(is_nano: bool, nano_steps: int, stax_steps: int, reject: bool
return moves return moves
def snapshot_test_name(nft_type: str, fn: str, chain_id: int, reject: bool) -> str: def snapshot_test_name(nft_type: str, fn: str, chain_id: int, reject: bool) -> str:
name = "%s_%s_%s" % (nft_type, fn.split("(")[0], str(chain_id)) name = "%s_%s_%s" % (nft_type, fn, str(chain_id))
if reject: if reject:
name += "-rejected" name += "-rejected"
return name return name
@@ -71,34 +77,48 @@ def common_test_nft(fw: Firmware,
action: Action, action: Action,
reject: bool, reject: bool,
plugin_name: str): plugin_name: str):
global DEVICE_ADDR
app_client = EthAppClient(back) app_client = EthAppClient(back)
selector = function_signature_to_4byte_selector(action.fn)
if app_client._client.firmware.name == "nanos": if app_client._client.firmware.name == "nanos":
pytest.skip("Not supported on LNS") pytest.skip("Not supported on LNS")
if DEVICE_ADDR is None: # to only have to request it once
with app_client.get_public_addr(display=False):
pass
_, DEVICE_ADDR, _ = ResponseParser.pk_addr(app_client.response().data)
data = collec.contract.encodeABI(action.fn_name, action.fn_args)
with app_client.set_plugin(plugin_name, with app_client.set_plugin(plugin_name,
collec.addr, collec.addr,
selector, get_selector_from_data(data),
1): collec.chain_id):
pass pass
with app_client.provide_nft_metadata(collec.name, collec.addr, collec.chain_id): with app_client.provide_nft_metadata(collec.name, collec.addr, collec.chain_id):
pass pass
with app_client.sign_legacy(BIP32_PATH, tx_params = {
NONCE, "nonce": NONCE,
GAS_PRICE, "gasPrice": Web3.to_wei(GAS_PRICE, "gwei"),
GAS_LIMIT, "gas": GAS_LIMIT,
collec.addr, "to": collec.addr,
0, "value": 0,
collec.chain_id, "chainId": collec.chain_id,
action.data_fn(action)): "data": data,
}
with app_client.sign(BIP32_PATH, tx_params):
nav.navigate_and_compare(ROOT_SCREENSHOT_PATH, nav.navigate_and_compare(ROOT_SCREENSHOT_PATH,
snapshot_test_name(plugin_name.lower(), snapshot_test_name(plugin_name.lower(),
action.fn, action.fn_name,
collec.chain_id, collec.chain_id,
reject), reject),
action.nav_fn(fw.is_nano, action.nav_fn(fw.is_nano,
collec.chain_id, collec.chain_id,
reject)) reject))
# verify signature
vrs = ResponseParser.signature(app_client.response().data)
addr = recover_transaction(tx_params, vrs)
assert addr == DEVICE_ADDR
def common_test_nft_reject(test_fn: Callable, def common_test_nft_reject(test_fn: Callable,
fw: Firmware, fw: Firmware,
@@ -116,48 +136,14 @@ def common_test_nft_reject(test_fn: Callable,
# ERC-721 # ERC-721
ERC721_PLUGIN = "ERC721" ERC721_PLUGIN = "ERC721"
ERC721_SAFE_TRANSFER_FROM_DATA = "safeTransferFrom(address,address,uint256,bytes)"
ERC721_SAFE_TRANSFER_FROM = "safeTransferFrom(address,address,uint256)"
ERC721_TRANSFER_FROM = "transferFrom(address,address,uint256)"
ERC721_APPROVE = "approve(address,uint256)"
ERC721_SET_APPROVAL_FOR_ALL = "setApprovalForAll(address,bool)"
## data formatting functions with open("%s/erc721.json" % (ABIS_FOLDER)) as file:
contract_erc721 = Web3().eth.contract(
def data_erc721_transfer_from(action: Action) -> TxData: abi=json.load(file),
return TxData( address=bytes(20)
function_signature_to_4byte_selector(action.fn),
[
FROM,
TO,
struct.pack(">H", NFTS[0][0])
]
) )
def data_erc721_safe_transfer_from_data(action: Action) -> TxData: # ui nav functions
txd = data_erc721_transfer_from(action)
txd.parameters += [ DATA ]
return txd
def data_erc721_approve(action: Action) -> TxData:
return TxData(
function_signature_to_4byte_selector(action.fn),
[
TO,
struct.pack(">H", NFTS[0][0])
]
)
def data_erc721_set_approval_for_all(action: Action) -> TxData:
return TxData(
function_signature_to_4byte_selector(action.fn),
[
TO,
struct.pack("b", False)
]
)
## ui nav functions
def nav_erc721_transfer_from(is_nano: bool, def nav_erc721_transfer_from(is_nano: bool,
chain_id: int, chain_id: int,
@@ -190,30 +176,33 @@ def nav_erc721_set_approval_for_all(is_nano: bool,
collecs_721 = [ collecs_721 = [
NFTCollection(bytes.fromhex("bc4ca0eda7647a8ab7c2061c2e118a18a936f13d"), NFTCollection(bytes.fromhex("bc4ca0eda7647a8ab7c2061c2e118a18a936f13d"),
"Bored Ape Yacht Club", "Bored Ape Yacht Club",
1), 1,
contract_erc721),
NFTCollection(bytes.fromhex("670fd103b1a08628e9557cd66b87ded841115190"), NFTCollection(bytes.fromhex("670fd103b1a08628e9557cd66b87ded841115190"),
"y00ts", "y00ts",
137), 137,
contract_erc721),
NFTCollection(bytes.fromhex("2909cf13e458a576cdd9aab6bd6617051a92dacf"), NFTCollection(bytes.fromhex("2909cf13e458a576cdd9aab6bd6617051a92dacf"),
"goerlirocks", "goerlirocks",
5) 5,
contract_erc721),
] ]
actions_721 = [ actions_721 = [
Action(ERC721_SAFE_TRANSFER_FROM_DATA, Action("safeTransferFrom",
data_erc721_safe_transfer_from_data, [FROM, TO, NFTS[0][0], DATA],
nav_erc721_transfer_from), nav_erc721_transfer_from),
Action(ERC721_SAFE_TRANSFER_FROM, Action("safeTransferFrom",
data_erc721_transfer_from, [FROM, TO, NFTS[0][0]],
nav_erc721_transfer_from), nav_erc721_transfer_from),
Action(ERC721_TRANSFER_FROM, Action("transferFrom",
data_erc721_transfer_from, [FROM, TO, NFTS[0][0]],
nav_erc721_transfer_from), nav_erc721_transfer_from),
Action(ERC721_APPROVE, Action("approve",
data_erc721_approve, [TO, NFTS[0][0]],
nav_erc721_approve), nav_erc721_approve),
Action(ERC721_SET_APPROVAL_FOR_ALL, Action("setApprovalForAll",
data_erc721_set_approval_for_all, [TO, False],
nav_erc721_set_approval_for_all) nav_erc721_set_approval_for_all),
] ]
@@ -251,51 +240,15 @@ def test_erc721_reject(firmware: Firmware,
# ERC-1155 # ERC-1155
ERC1155_PLUGIN = "ERC1155" ERC1155_PLUGIN = "ERC1155"
ERC1155_SAFE_TRANSFER_FROM = "safeTransferFrom(address,address,uint256,uint256,bytes)"
ERC1155_SAFE_BATCH_TRANSFER_FROM = "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)"
ERC1155_SET_APPROVAL_FOR_ALL = "setApprovalForAll(address,bool)"
## data formatting functions with open("%s/erc1155.json" % (ABIS_FOLDER)) as file:
contract_erc1155 = Web3().eth.contract(
def data_erc1155_safe_transfer_from(action: Action) -> TxData: abi=json.load(file),
return TxData( address=bytes(20)
function_signature_to_4byte_selector(action.fn),
[
FROM,
TO,
struct.pack(">H", NFTS[0][0]),
struct.pack(">H", NFTS[0][1]),
DATA
]
) )
def data_erc1155_safe_batch_transfer_from(action: Action) -> TxData:
data = TxData(
function_signature_to_4byte_selector(action.fn),
[
FROM,
TO
])
data.parameters += [ int(32 * 4).to_bytes(8, "big") ] # token_ids offset
data.parameters += [int(32 * (4 + len(NFTS) + 1)).to_bytes(8, "big") ] # amounts offset
data.parameters += [ int(len(NFTS)).to_bytes(8, "big") ] # token_ids length
for nft in NFTS:
data.parameters += [ struct.pack(">H", nft[0]) ] # token_id
data.parameters += [ int(len(NFTS)).to_bytes(8, "big") ] # amounts length
for nft in NFTS:
data.parameters += [ struct.pack(">H", nft[1]) ] # amount
return data
def data_erc1155_set_approval_for_all(action: Action) -> TxData: # ui nav functions
return TxData(
function_signature_to_4byte_selector(action.fn),
[
TO,
struct.pack("b", False)
]
)
## ui nav functions
def nav_erc1155_safe_transfer_from(is_nano: bool, def nav_erc1155_safe_transfer_from(is_nano: bool,
chain_id: int, chain_id: int,
@@ -326,24 +279,33 @@ def nav_erc1155_set_approval_for_all(is_nano: bool,
collecs_1155 = [ collecs_1155 = [
NFTCollection(bytes.fromhex("495f947276749ce646f68ac8c248420045cb7b5e"), NFTCollection(bytes.fromhex("495f947276749ce646f68ac8c248420045cb7b5e"),
"OpenSea Shared Storefront", "OpenSea Shared Storefront",
1), 1,
contract_erc1155),
NFTCollection(bytes.fromhex("2953399124f0cbb46d2cbacd8a89cf0599974963"), NFTCollection(bytes.fromhex("2953399124f0cbb46d2cbacd8a89cf0599974963"),
"OpenSea Collections", "OpenSea Collections",
137), 137,
contract_erc1155),
NFTCollection(bytes.fromhex("f4910c763ed4e47a585e2d34baa9a4b611ae448c"), NFTCollection(bytes.fromhex("f4910c763ed4e47a585e2d34baa9a4b611ae448c"),
"OpenSea Collections", "OpenSea Collections",
5) 5,
contract_erc1155),
] ]
actions_1155 = [ actions_1155 = [
Action(ERC1155_SAFE_TRANSFER_FROM, Action("safeTransferFrom",
data_erc1155_safe_transfer_from, [FROM, TO, NFTS[0][0], NFTS[0][1], DATA],
nav_erc1155_safe_transfer_from), nav_erc1155_safe_transfer_from),
Action(ERC1155_SAFE_BATCH_TRANSFER_FROM, Action("safeBatchTransferFrom",
data_erc1155_safe_batch_transfer_from, [
FROM,
TO,
list(map(lambda nft: nft[0], NFTS)),
list(map(lambda nft: nft[1], NFTS)),
DATA
],
nav_erc1155_safe_batch_transfer_from), nav_erc1155_safe_batch_transfer_from),
Action(ERC1155_SET_APPROVAL_FOR_ALL, Action("setApprovalForAll",
data_erc1155_set_approval_for_all, [TO, False],
nav_erc1155_set_approval_for_all) nav_erc1155_set_approval_for_all),
] ]
@pytest.fixture(params=collecs_1155) @pytest.fixture(params=collecs_1155)
def collec_1155(request) -> bool: def collec_1155(request) -> bool: