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