Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/build_and_functional_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ on:
- develop
pull_request:

permissions:
contents: read
packages: read

jobs:
build_application:
name: Build application using the reusable workflow
Expand All @@ -41,3 +45,12 @@ jobs:
with:
download_app_binaries_artifact: "app_tezos_binaries"
regenerate_snapshots: ${{ github.event_name == 'workflow_dispatch' && inputs.golden_run == 'Open a PR' }}
test_dir: "tests/standalone"

tests_swap:
name: Run swap tests using the reusable workflow
needs: build_application
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_swap_tests.yml@v1
with:
download_app_binaries_artifact: "app_tezos_binaries"
regenerate_snapshots: ${{ github.event_name == 'workflow_dispatch' && inputs.golden_run == 'Open a PR' }}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ integration_tests_log
pattern_registry
tests/samples
tests/standalone/snapshots-tmp
tests/swap/snapshots-tmp
tests/swap/.test_dependencies/
unit-tests/build
*env/
ledger/
Expand Down
2 changes: 1 addition & 1 deletion app/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ APPNAME = "Tezos Wallet"
# Application version
APPVERSION_M=3
APPVERSION_N=2
APPVERSION_P=2
APPVERSION_P=4
APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)

# COMMIT
Expand Down
89 changes: 80 additions & 9 deletions app/src/handle_swap.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@
#include "keys.h"
#include "utils.h"

#include "parser/fa2_tokens.h"
#include "parser/num_parser.h"

// based on app-exchange
#define TICKER "XTZ"
#define ADDRESS_MAX_SIZE 63
/* the smallest unit is microtez */
#define DECIMALS 6
/* Room for the ticker of a token swap, see swap_parse_config() */
#define TICKER_MAX_SIZE 16

/* Check check_address_parameters_t.address_to_check against specified
* parameters.
Expand Down Expand Up @@ -99,6 +102,20 @@ swap_handle_get_printable_amount(get_printable_amount_parameters_t *params)
FUNC_ENTER(("params=%p", params));

uint64_t amount;
char ticker[TICKER_MAX_SIZE] = TICKER;
uint8_t decimals = DECIMALS;

/* Fees are always paid in tez, even when swapping a token. Without a coin
* configuration the currency is tez too. */
if (!params->is_fee && (params->coin_configuration != NULL)
&& (params->coin_configuration_length > 0)) {
if (!swap_parse_config(params->coin_configuration,
params->coin_configuration_length, ticker,
sizeof(ticker), &decimals)) {
PRINTF("[ERROR] Fail to parse coin configuration\n");
goto error;
}
}

if (!swap_str_to_u64(params->amount, params->amount_length, &amount)) {
PRINTF("[ERROR] Fail to parse amount\n");
Expand All @@ -107,13 +124,13 @@ swap_handle_get_printable_amount(get_printable_amount_parameters_t *params)

if (!format_fpu64_trimmed(params->printable_amount,
sizeof(params->printable_amount), amount,
DECIMALS)) {
decimals)) {
PRINTF("[ERROR] Fail to print amount\n");
goto error;
}

strlcat(params->printable_amount, " ", sizeof(params->printable_amount));
strlcat(params->printable_amount, TICKER,
strlcat(params->printable_amount, ticker,
sizeof(params->printable_amount));

FUNC_LEAVE();
Expand All @@ -128,6 +145,11 @@ typedef struct {
uint64_t amount;
uint64_t fee; /// Contains transaction fees plus reveal fees, if any.
char destination_address[ADDRESS_MAX_SIZE];
/// Set when the Exchange application gave us a coin configuration, i.e.
/// when the currency being sent is an FA2 token rather than tez.
bool is_token;
char ticker[TICKER_MAX_SIZE]; /// ticker of that token
uint8_t decimals; /// its number of decimals
} swap_transaction_parameters_t;

static swap_transaction_parameters_t G_swap_params;
Expand Down Expand Up @@ -158,6 +180,21 @@ swap_copy_transaction_parameters(create_transaction_parameters_t *params)
goto error;
}

/* A coin configuration means the swap sends a token, not tez. Without one
* we have no way to tell which token an FA2 transfer moves, so token
* swaps are only accepted when the Exchange application provides it. */
if ((params->coin_configuration != NULL)
&& (params->coin_configuration_length > 0)) {
if (!swap_parse_config(params->coin_configuration,
params->coin_configuration_length,
params_copy.ticker, sizeof(params_copy.ticker),
&params_copy.decimals)) {
PRINTF("[ERROR] Fail to parse coin configuration\n");
goto error;
}
params_copy.is_token = true;
}

if (params->destination_address == NULL) {
PRINTF("[ERROR] Destination address is null\n");
goto error;
Expand Down Expand Up @@ -208,15 +245,49 @@ swap_check_validity(void)
TZ_ASSERT(EXC_REJECT, op->nb_reveal <= 1);
TZ_ASSERT(EXC_REJECT, (op->batch_index - op->nb_reveal) == 1);
TZ_ASSERT(EXC_REJECT, op->last_tag == TZ_OPERATION_TAG_TRANSACTION);
TZ_ASSERT(EXC_REJECT, op->total_amount == G_swap_params.amount);
TZ_ASSERT(EXC_REJECT, op->total_fee == G_swap_params.fee);

tz_format_address(op->destination, 22, dstaddr, sizeof(dstaddr));

PRINTF("[DEBUG] dstaddr=\"%s\"\n", dstaddr);
PRINTF("[DEBUG] G...dstaddr=\"%s\"\n", G_swap_params.destination_address);
TZ_ASSERT(EXC_REJECT,
!strcmp(dstaddr, G_swap_params.destination_address));
if (G_swap_params.is_token) {
/* A token swap is an FA2 `transfer` call on the token contract: the
* operation carries no tez, its destination is the contract, and the
* recipient and the amount live in the Michelson parameters. */
const fa2_token_metadata_t *token;

TZ_ASSERT(EXC_REJECT, op->total_amount == 0);

/* Set only when the parser decoded a single, complete transfer.
* Anything else - several transfers, an unsupported encoding, an
* amount we cannot represent - leaves it clear. */
TZ_ASSERT(EXC_REJECT, op->fa2_swap_ok);

/* The token being moved must be the one the swap was quoted for. The
* ticker comes from the Ledger-signed coin configuration, so matching
* it against the registry entry of the contract actually called ties
* the two together. */
token = fa2_find_token(op->destination, op->fa2_token_id);
TZ_ASSERT(EXC_REJECT, token != NULL);
PRINTF("[DEBUG] token=\"%s\" ticker=\"%s\"\n", token->symbol,
G_swap_params.ticker);
TZ_ASSERT(EXC_REJECT, !strcmp(token->symbol, G_swap_params.ticker));
TZ_ASSERT(EXC_REJECT, token->decimals == G_swap_params.decimals);

PRINTF("[DEBUG] fa2 dstaddr=\"%s\"\n", op->fa2_destination);
PRINTF("[DEBUG] G...dstaddr=\"%s\"\n",
G_swap_params.destination_address);
TZ_ASSERT(EXC_REJECT, !strcmp(op->fa2_destination,
G_swap_params.destination_address));
TZ_ASSERT(EXC_REJECT, op->fa2_amount == G_swap_params.amount);
} else {
TZ_ASSERT(EXC_REJECT, op->total_amount == G_swap_params.amount);

tz_format_address(op->destination, 22, dstaddr, sizeof(dstaddr));

PRINTF("[DEBUG] dstaddr=\"%s\"\n", dstaddr);
PRINTF("[DEBUG] G...dstaddr=\"%s\"\n",
G_swap_params.destination_address);
TZ_ASSERT(EXC_REJECT,
!strcmp(dstaddr, G_swap_params.destination_address));
}

TZ_POSTAMBLE;
}
Expand Down
67 changes: 65 additions & 2 deletions app/src/parser/operation_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,12 @@ tz_operation_parser_init(tz_parser_state *state, uint16_t size,
memset(&state->operation.destination, 0, 22);
op->batch_index = 0;
#ifdef HAVE_SWAP
op->last_tag = TZ_OPERATION_TAG_END;
op->nb_reveal = 0;
op->last_tag = TZ_OPERATION_TAG_END;
op->nb_reveal = 0;
op->fa2_swap_ok = 0;
op->fa2_token_id = 0;
op->fa2_amount = 0;
op->fa2_destination[0] = '\0';
#endif // HAVE_SWAP
op->total_fee = 0;
op->total_amount = 0;
Expand Down Expand Up @@ -623,6 +627,9 @@ tz_step_tag(tz_parser_state *state)
if (t == TZ_OPERATION_TAG_REVEAL) {
op->nb_reveal++;
}
/* Reset per-operation: a transfer decoded in an earlier operation of the
batch must not be credited to this one. */
op->fa2_swap_ok = 0;
#endif // HAVE_SWAP
op->is_fa2_candidate = 0;
memset(&op->destination, 0, TZ_OPERATION_DESTINATION_SIZE);
Expand Down Expand Up @@ -680,6 +687,45 @@ tz_step_tag(tz_parser_state *state)
_Static_assert((TZ_CAPTURE_BUFFER_SIZE % 2U) == 0U,
"TZ_CAPTURE_BUFFER_SIZE must be even for FA2 CAPTURE split");

#ifdef HAVE_SWAP
/**
* @brief Convert a decimal ASCII string to a uint64.
*
* Used to keep an FA2 amount in a form swap validation can compare
* against the amount validated by the Exchange application.
*
* @param str: NUL-terminated decimal digits
* @param out: parsed value, untouched on failure
* @return bool: false if empty, not made of digits only (a leading `-`
* included), or larger than UINT64_MAX
*/
static bool
fa2_decimal_to_u64(const char *str, uint64_t *out)
{
uint64_t value = 0;

if ((str == NULL) || (*str == '\0')) {
return false;
}

for (; *str != '\0'; str++) {
uint64_t digit;

if ((*str < '0') || (*str > '9')) {
return false;
}
digit = (uint64_t)(*str - '0');
if (value > ((UINT64_MAX - digit) / 10u)) {
return false;
}
value = (value * 10u) + digit;
}

*out = value;
return true;
}
#endif // HAVE_SWAP

/**
* @brief Format an integer token amount string with token decimals and
* symbol.
Expand Down Expand Up @@ -1129,6 +1175,23 @@ tz_step_read_fa2_transfer(tz_parser_state *state)
/* Verify we are at the end of the parameter (no extra items) */
FA2_REQUIRE(state, state->ofs == op->frame->stop);

#ifdef HAVE_SWAP
/* Save what swap validation needs before the emit steps below run:
they reuse both the CAPTURE buffer and the decimal buffer, and the
frame holding token_id is popped on the way out. Anything we cannot
represent leaves fa2_swap_ok clear, which makes a swap refuse the
operation while leaving the display path untouched. */
if (fa2_decimal_to_u64((const char *)state->buffers.num.decimal,
&op->fa2_amount)
&& (strlen((const char *)(CAPTURE + FA2_TO_ADDR_OFS))
< sizeof(op->fa2_destination))) {
STRLCPY(op->fa2_destination,
(const char *)(CAPTURE + FA2_TO_ADDR_OFS));
op->fa2_token_id = op->frame->step_read_fa2.token_id_val;
op->fa2_swap_ok = 1;
}
#endif // HAVE_SWAP

op->frame->step_read_fa2.sub_step = FA2_STEP_EMIT_TO_ADDR;
tz_continue;

Expand Down
17 changes: 14 additions & 3 deletions app/src/parser/operation_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ typedef struct {
#define TZ_OPERATION_SOURCE_SIZE 22
#define TZ_OPERATION_DESTINATION_SIZE 22

/// Base58 Tezos addresses (tz1/tz2/tz3/tz4/KT1) are 36 characters long
#define TZ_OPERATION_FA2_ADDR_SIZE 37

/**
* @brief This struct represents the parser of operations
*
Expand Down Expand Up @@ -288,7 +291,15 @@ typedef struct {
#ifdef HAVE_SWAP
tz_operation_tag last_tag; /// last operations tag encountered
uint16_t nb_reveal; /// number of reveal encountered
#endif // HAVE_SWAP
uint64_t total_fee; /// last fee encountered
uint64_t total_amount; /// last amount encountered
/// A single FA2 `transfer` was decoded in full. The three fields below
/// are meaningful only then. They are kept here, and not in the parser
/// frame, because swap validation reads them once parsing is over: see
/// swap_check_validity() in handle_swap.c.
uint8_t fa2_swap_ok : 1;
uint64_t fa2_token_id; /// token id of that transfer
uint64_t fa2_amount; /// its amount, in the token's smallest unit
char fa2_destination[TZ_OPERATION_FA2_ADDR_SIZE]; /// its `to_`
#endif // HAVE_SWAP
uint64_t total_fee; /// last fee encountered
uint64_t total_amount; /// last amount encountered
} tz_operation_state;
8 changes: 8 additions & 0 deletions ledger_app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ directory = "./unit-tests/"

[pytest.standalone]
directory = "./tests/standalone/"

[pytest.swap]
directory = "./tests/swap/"
[pytest.swap.dependencies]
testing_with_latest = [
{ url = "https://github.com/LedgerHQ/app-exchange", ref = "develop", use_case = "dbg_use_test_keys" },
{ url = "https://github.com/LedgerHQ/app-ethereum", ref = "develop", use_case = "use_test_keys" },
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/standalone/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

def test_version(backend: TezosBackend):
"""Test that the app version is the same as the current version."""
current_version = Version(Version.AppKind.WALLET, 3, 2, 2)
current_version = Version(Version.AppKind.WALLET, 3, 2, 4)

data = backend.version()

Expand Down
Loading
Loading