Skip to content

Commit

Permalink
Merge pull request #627 from LedgerHQ/fbe/thorswap_on_eth
Browse files Browse the repository at this point in the history
Implement swap with calldata
  • Loading branch information
fbeutin-ledger authored Aug 23, 2024
2 parents 4c9b91d + abaa11e commit 055e0c0
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 9 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.12.0](https://github.com/ledgerhq/app-ethereum/compare/1.11.0...1.11.1) - 2024-??-??

### Added

- Added support for swap with calldata (Thorswap / LiFi / ...)

## [1.11.2](https://github.com/ledgerhq/app-ethereum/compare/1.11.1...1.11.2) - 2024-08-13

### Added
Expand Down
14 changes: 11 additions & 3 deletions src/eth_plugin_handler.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "plugin_utils.h"
#include "shared_context.h"
#include "network.h"
#include "cmd_setPlugin.h"

void eth_plugin_prepare_init(ethPluginInitContract_t *init,
const uint8_t *selector,
Expand Down Expand Up @@ -153,10 +154,13 @@ eth_plugin_result_t eth_plugin_perform_init(uint8_t *contractAddress,
break;
}

// Do not handle a plugin if running in swap mode
if (G_called_from_swap && (contractAddress != NULL)) {
PRINTF("eth_plug_init aborted in swap mode\n");
return 0;
PRINTF("contractAddress == %.*H\n", 20, contractAddress);
PRINTF("selector == %.*H\n", 20, contractAddress);
PRINTF("Fallback on swap_with_calldata plugin\n");
set_swap_with_calldata_plugin_type();
dataContext.tokenContext.pluginStatus = ETH_PLUGIN_RESULT_OK;
contractAddress = NULL;
}

eth_plugin_result_t status = ETH_PLUGIN_RESULT_UNAVAILABLE;
Expand Down Expand Up @@ -271,6 +275,10 @@ eth_plugin_result_t eth_plugin_call(int method, void *parameter) {
END_TRY;
break;
}
case SWAP_WITH_CALLDATA: {
swap_with_calldata_plugin_call(method, parameter);
break;
}
#ifdef HAVE_NFT_SUPPORT
case ERC721: {
erc721_plugin_call(method, parameter);
Expand Down
1 change: 1 addition & 0 deletions src/eth_plugin_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

void erc721_plugin_call(int message, void* parameters);
void erc1155_plugin_call(int message, void* parameters);
void swap_with_calldata_plugin_call(int message, void* parameters);

typedef bool (*const PluginAvailableCheck)(void);
typedef void (*PluginCall)(int, void*);
Expand Down
53 changes: 53 additions & 0 deletions src/handle_swap_sign_transaction.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,37 @@
#include "nbgl_use_case.h"
#endif // HAVE_NBGL

// Remember if we have been started by the Exchange application or not
bool G_called_from_swap;

// Set this boolean when a transaction is signed in Swap mode. Safety against double sign
bool G_swap_response_ready;

// Save the BSS address where we will write the return value when finished
static uint8_t* G_swap_sign_return_value_address;

// Standard or crosschain swap type
swap_mode_t G_swap_mode;

// On crosschain swap, save the hash promised by the partner
uint8_t G_swap_crosschain_hash[CX_SHA256_SIZE];

typedef enum extra_id_type_e {
EXTRA_ID_TYPE_NATIVE,
EXTRA_ID_TYPE_EVM_CALLDATA,
// There are others but they are not relevant for the Ethereum application
} extra_id_type_t;

bool copy_transaction_parameters(create_transaction_parameters_t* sign_transaction_params,
const chain_config_t* config) {
// first copy parameters to stack, and then to global data.
// We need this "trick" as the input data position can overlap with app-ethereum globals
txStringProperties_t stack_data;
uint8_t destination_address_extra_data[CX_SHA256_SIZE + 1];

memset(&stack_data, 0, sizeof(stack_data));
memset(destination_address_extra_data, 0, sizeof(destination_address_extra_data));

strlcpy(stack_data.toAddress,
sign_transaction_params->destination_address,
sizeof(stack_data.toAddress));
Expand All @@ -28,6 +50,12 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti
return false;
}

if (sign_transaction_params->destination_address_extra_id != NULL) {
memcpy(destination_address_extra_data,
sign_transaction_params->destination_address_extra_id,
sizeof(destination_address_extra_data));
}

char ticker[MAX_TICKER_LEN];
uint8_t decimals;
uint64_t chain_id = 0;
Expand Down Expand Up @@ -71,6 +99,31 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti
G_swap_sign_return_value_address = &sign_transaction_params->result;
// Commit the values read from exchange to the clean global space

// if destination_address_extra_id is given, we use the first byte to determine if we use the
// normal swap protocol, or the one for cross-chain swaps
switch (destination_address_extra_data[0]) {
case EXTRA_ID_TYPE_NATIVE:
G_swap_mode = SWAP_MODE_STANDARD;
PRINTF("Standard swap\n");

// we don't use the payin_extra_id field in this mode
explicit_bzero(G_swap_crosschain_hash, sizeof(G_swap_crosschain_hash));
break;
case EXTRA_ID_TYPE_EVM_CALLDATA:
G_swap_mode = SWAP_MODE_CROSSCHAIN_PENDING_CHECK;

memcpy(G_swap_crosschain_hash,
destination_address_extra_data + 1,
sizeof(G_swap_crosschain_hash));

PRINTF("Crosschain swap with hash: %.*H\n", CX_SHA256_SIZE, G_swap_crosschain_hash);
break;
default:
// We can't return errors from here, we remember that we have an issue to report later
PRINTF("Invalid or unknown swap protocol\n");
G_swap_mode = SWAP_MODE_ERROR;
}

memcpy(&strings.common, &stack_data, sizeof(stack_data));
return true;
}
Expand Down
2 changes: 0 additions & 2 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ cx_sha3_t global_sha3;

uint8_t appState;
uint16_t apdu_response_code;
bool G_called_from_swap;
bool G_swap_response_ready;
pluginType_t pluginType;

#ifdef HAVE_ETH2
Expand Down
24 changes: 20 additions & 4 deletions src/shared_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,30 @@ extern strings_t strings;
extern cx_sha3_t global_sha3;
extern const internalStorage_t N_storage_real;

typedef enum swap_mode_e {
SWAP_MODE_STANDARD,
SWAP_MODE_CROSSCHAIN_PENDING_CHECK,
SWAP_MODE_CROSSCHAIN_SUCCESS,
SWAP_MODE_ERROR,
} swap_mode_t;

extern bool G_called_from_swap;
extern bool G_swap_response_ready;
extern swap_mode_t G_swap_mode;
extern uint8_t G_swap_crosschain_hash[CX_SHA256_SIZE];

typedef enum {
EXTERNAL, // External plugin, set by setExternalPlugin.
ERC721, // Specific ERC721 internal plugin, set by setPlugin.
ERC1155, // Specific ERC1155 internal plugin, set by setPlugin
OLD_INTERNAL // Old internal plugin, not set by any command.
// External plugin, set by setExternalPlugin
EXTERNAL,
// Specific SWAP_WITH_CALLDATA internal plugin
// set as fallback when started if calldata is provided in swap mode
SWAP_WITH_CALLDATA,
// Specific ERC721 internal plugin, set by setPlugin
ERC721,
// Specific ERC1155 internal plugin, set by setPlugin
ERC1155,
// Old internal plugin, not set by any command
OLD_INTERNAL
} pluginType_t;

extern pluginType_t pluginType;
Expand Down
8 changes: 8 additions & 0 deletions src_features/setPlugin/cmd_setPlugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,23 @@ typedef bool verificationAlgo(const cx_ecfp_public_key_t *,
static pluginType_t getPluginType(char *pluginName, uint8_t pluginNameLength) {
if (pluginNameLength == sizeof(ERC721_STR) - 1 &&
strncmp(pluginName, ERC721_STR, pluginNameLength) == 0) {
PRINTF("Using internal plugin ERC721\n");
return ERC721;
} else if (pluginNameLength == sizeof(ERC1155_STR) - 1 &&
strncmp(pluginName, ERC1155_STR, pluginNameLength) == 0) {
PRINTF("Using internal plugin ERC1155\n");
return ERC1155;
} else {
PRINTF("Using external plugin\n");
return EXTERNAL;
}
}

void set_swap_with_calldata_plugin_type(void) {
PRINTF("Using internal plugin SWAP_WITH_CALLDATA\n");
pluginType = SWAP_WITH_CALLDATA;
}

void handleSetPlugin(uint8_t p1,
uint8_t p2,
const uint8_t *workBuffer,
Expand Down
3 changes: 3 additions & 0 deletions src_features/setPlugin/cmd_setPlugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

void set_swap_with_calldata_plugin_type(void);
16 changes: 16 additions & 0 deletions src_features/signTx/logic_signTx.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ __attribute__((noinline)) static bool finalize_parsing_helper(void) {
pluginFinalize.result = pluginProvideInfo.result;
}
if (pluginFinalize.result != ETH_PLUGIN_RESULT_FALLBACK) {
PRINTF("pluginFinalize.result %d successful\n", pluginFinalize.result);
// Handle the right interface
switch (pluginFinalize.uiType) {
case ETH_UI_TYPE_GENERIC:
Expand Down Expand Up @@ -423,6 +424,10 @@ __attribute__((noinline)) static bool finalize_parsing_helper(void) {
report_finalize_error();
return false;
}
} else if (G_called_from_swap && G_swap_mode == SWAP_MODE_CROSSCHAIN_SUCCESS) {
PRINTF("Plugin swap_with_calldata fell back for UI with success\n");
// We are not bling signing, the data has been validated by the plugin
tmpContent.txContent.dataPresent = false;
}
}

Expand All @@ -441,7 +446,18 @@ __attribute__((noinline)) static bool finalize_parsing_helper(void) {
THROW(ERR_SILENT_MODE_CHECK_FAILED);
}

// Specific calldata check when in swap mode
if (G_called_from_swap) {
// Two success cases: we are in standard mode and no calldata was received
// We are in crosschain mode and the correct calldata has been received
if (G_swap_mode != SWAP_MODE_STANDARD && G_swap_mode != SWAP_MODE_CROSSCHAIN_SUCCESS) {
PRINTF("Error: G_swap_mode %d refused\n", G_swap_mode);
THROW(ERR_SILENT_MODE_CHECK_FAILED);
}
}

if (tmpContent.txContent.dataPresent && !N_storage.dataAllowed) {
PRINTF("Data is present but not allowed\n");
report_finalize_error();
ui_error_blind_signing();
return false;
Expand Down
137 changes: 137 additions & 0 deletions src_plugins/swap_with_calldata_plugin.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include <string.h>
#include "lib_cxng/src/cx_sha256.h"
#include "plugin_utils.h"
#include "eth_plugin_internal.h"
#include "eth_plugin_interface.h"
#include "eth_plugin_handler.h"

/* SWAP_WITH_CALLDATA 'internal plugin'
*
* This internal plugin is used to parse calldata in the SWAP_WITH_CALLDATA context.
* Used for example by Thorswap and LiFi.
* Difference with main swap mode:
* - The swap transaction promise made by the partner contains a 32 bytes hash additional field
* - This hash is the hash of the calldata of the final signing transaction
* - During the final TX signature in ETH, we check the destination and amount as usual
* - We also check the hash of the calldata against the promised hash.
*
* This minimalist internal plugin is used to do this calldata hashing and check.
*/

typedef struct swap_with_calldata_context_s {
// Hash of the calldata received.
// Computed on (Selector + N * 32bytes parameters)
// We don't care at all about the content, we'll just compare the hash with the promised hash.
cx_sha256_t update_hash;
} swap_with_calldata_context_t;

void handle_init_contract_swap_with_calldata(ethPluginInitContract_t *msg) {
PRINTF("handle_init_contract_swap_with_calldata\n");
if (!G_called_from_swap) {
// Can't happen in theory, but let's double check.
PRINTF("swap_with_calldata plugin can't be used outside of SWAP context");
msg->result = ETH_PLUGIN_RESULT_ERROR;
return;
}

swap_with_calldata_context_t *context = (swap_with_calldata_context_t *) msg->pluginContext;

PRINTF("swap_with_calldata plugin init contract %.*H\n", SELECTOR_SIZE, msg->selector);

// Can't fail
cx_sha256_init_no_throw(&context->update_hash);

if (cx_sha256_update(&context->update_hash, msg->selector, SELECTOR_SIZE) != CX_OK) {
PRINTF("ERROR: cx_sha256_update on selector\n");
msg->result = ETH_PLUGIN_RESULT_ERROR;
} else {
msg->result = ETH_PLUGIN_RESULT_OK;
}
}

void handle_provide_parameter_swap_with_calldata(ethPluginProvideParameter_t *msg) {
PRINTF("handle_provide_parameter_swap_with_calldata\n");
if (!G_called_from_swap) {
// Can't happen in theory, but let's double check.
PRINTF("swap_with_calldata plugin can't be used outside of SWAP context");
msg->result = ETH_PLUGIN_RESULT_ERROR;
return;
}

swap_with_calldata_context_t *context = (swap_with_calldata_context_t *) msg->pluginContext;

PRINTF("swap_with_calldata plugin provide parameter %d %.*H\n",
msg->parameterOffset,
PARAMETER_LENGTH,
msg->parameter);

if (cx_sha256_update(&context->update_hash, msg->parameter, PARAMETER_LENGTH) != CX_OK) {
PRINTF("ERROR: cx_sha256_update on parameter %d\n", msg->parameterOffset);
msg->result = ETH_PLUGIN_RESULT_ERROR;
} else {
msg->result = ETH_PLUGIN_RESULT_OK;
}
}

void handle_finalize_swap_with_calldata(ethPluginFinalize_t *msg) {
PRINTF("handle_finalize_swap_with_calldata\n");
if (!G_called_from_swap) {
// Can't happen in theory, but let's double check.
PRINTF("swap_with_calldata plugin can't be used outside of SWAP context");
msg->result = ETH_PLUGIN_RESULT_ERROR;
return;
}

swap_with_calldata_context_t *context = (swap_with_calldata_context_t *) msg->pluginContext;

// Can't fail
uint8_t hash_digest[CX_SHA256_SIZE];
cx_sha256_final(&context->update_hash, hash_digest);

// Compare computed hash with promise made in Exchange by the partner
if (memcmp(hash_digest, G_swap_crosschain_hash, CX_SHA256_SIZE) != 0) {
PRINTF("ERROR: Wrong hash, promised %.*H, received %.*H\n",
CX_SHA256_SIZE,
G_swap_crosschain_hash,
CX_SHA256_SIZE,
hash_digest);
G_swap_mode = SWAP_MODE_ERROR;
} else {
PRINTF("Finalize successful, hashes match\n");
if (G_swap_mode == SWAP_MODE_CROSSCHAIN_PENDING_CHECK) {
// Remember that a calldata exists and is valid
// This differentiates the case were there was no calldata is provided
G_swap_mode = SWAP_MODE_CROSSCHAIN_SUCCESS;
} else {
PRINTF("G_swap_mode %d wrong, refusing to validate the calldata", G_swap_mode);
G_swap_mode = SWAP_MODE_ERROR;
}
}

// We use fallback so that the Eth UI takes back the display
// This is simpler than forwarding the values in txContent
// We return this even in case of errors because the error handling is done with G_swap_mode
msg->result = ETH_PLUGIN_RESULT_FALLBACK;
msg->tokenLookup1 = NULL;
msg->tokenLookup2 = NULL;
}

void swap_with_calldata_plugin_call(int message, void *parameters) {
switch (message) {
case ETH_PLUGIN_INIT_CONTRACT:
handle_init_contract_swap_with_calldata((ethPluginInitContract_t *) parameters);
break;
case ETH_PLUGIN_PROVIDE_PARAMETER:
handle_provide_parameter_swap_with_calldata((ethPluginProvideParameter_t *) parameters);
break;
case ETH_PLUGIN_FINALIZE:
handle_finalize_swap_with_calldata((ethPluginFinalize_t *) parameters);
break;
case ETH_PLUGIN_PROVIDE_INFO:
case ETH_PLUGIN_QUERY_CONTRACT_ID:
case ETH_PLUGIN_QUERY_CONTRACT_UI:
default:
PRINTF("Unhandled message %d\n", message);
break;
}
}

0 comments on commit 055e0c0

Please sign in to comment.