Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking] Refactor Ethereum token transfer signing input #1210

Merged
merged 44 commits into from
Dec 21, 2020
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1680fff
Factor out Transfer from Eth SigningInput.
catenocrypt Dec 3, 2020
395aaf6
Make with oneof, Transfer and ContractGeneric.
catenocrypt Dec 3, 2020
530893f
Rename
catenocrypt Dec 3, 2020
f8bde1c
Remove amount from generic contract.
catenocrypt Dec 3, 2020
d8ea9b4
Special ERC20 contract type (empty so far)
catenocrypt Dec 4, 2020
a40e76e
Various factory methods for Transaction class.
catenocrypt Dec 4, 2020
07662ff
Add new ERC20 test.
catenocrypt Dec 7, 2020
62bce71
Add ERC20 handling, building.
catenocrypt Dec 7, 2020
88031dd
ABI comment
catenocrypt Dec 7, 2020
5a65b7d
Rename in proto
catenocrypt Dec 7, 2020
7c99393
Update iOS tests.
catenocrypt Dec 7, 2020
292c2f7
Add ERC20 iOS test
catenocrypt Dec 7, 2020
0bd95a5
Update android tests
catenocrypt Dec 7, 2020
980b4e0
Update typescript test.
catenocrypt Dec 8, 2020
596aeae
Typescript test
catenocrypt Dec 8, 2020
7bd4b0c
Special handling for invalid Address case.
catenocrypt Dec 8, 2020
c5326f7
Additional low-level signer test.
catenocrypt Dec 8, 2020
e1444b8
Small refactor in transaction building.
catenocrypt Dec 8, 2020
da2c5a4
Android test fix.
catenocrypt Dec 8, 2020
77cfe49
Test rename
catenocrypt Dec 8, 2020
091ab21
SingerTest fix.
catenocrypt Dec 8, 2020
69ee3df
Support ERC721
catenocrypt Dec 8, 2020
8e441c7
ERC20 kotlin test
catenocrypt Dec 8, 2020
36b094a
Swift and Kotlin tests for ERC721
catenocrypt Dec 9, 2020
92c4cea
iOS 721 test fix
catenocrypt Dec 9, 2020
a2908db
iOS test fix
catenocrypt Dec 9, 2020
827c4ea
Refactor Eth proto (nested types).
catenocrypt Dec 9, 2020
21a9d9b
Renames in proto
catenocrypt Dec 9, 2020
fa0b71e
Typescript test fix
catenocrypt Dec 9, 2020
de7dbb9
Rename in proto
catenocrypt Dec 9, 2020
b66b78b
Comment in proto, expose ERC20 call building
catenocrypt Dec 10, 2020
e39528c
Add optional payload to plain Transfer.
catenocrypt Dec 10, 2020
34192df
ContractGeneric rename.
catenocrypt Dec 10, 2020
19dc8d9
Merge branch 'master' into ar/ethproto
catenocrypt Dec 10, 2020
7639bc0
Minor exception leak fix in Solana
catenocrypt Dec 10, 2020
7d5a03b
Adapt Aeternity and Aion
catenocrypt Dec 10, 2020
ba6d444
Revert "Adapt Aeternity and Aion"
catenocrypt Dec 11, 2020
3b87dcd
Payload -> Transaction rename.
catenocrypt Dec 14, 2020
8fe7501
Rename payload
catenocrypt Dec 14, 2020
378a2ef
Rename
catenocrypt Dec 14, 2020
fbc398a
Add ERC20 Approve
catenocrypt Dec 14, 2020
aa5a0e5
ERC20 approve iOS test
catenocrypt Dec 14, 2020
0bbb46e
Rename, remove transfer_ prefix
catenocrypt Dec 14, 2020
2647c6d
Typescript test fix
catenocrypt Dec 14, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ class TestEthereumTransactionSigner {
nonce = ByteString.copyFrom("0x9".toHexByteArray())
gasPrice = ByteString.copyFrom("0x04a817c800".toHexByteArray())
gasLimit = ByteString.copyFrom("0x5208".toHexByteArray())
amount = ByteString.copyFrom("0x0de0b6b3a7640000".toHexByteArray())
payload = Ethereum.Payload.newBuilder().apply {
payloadTransfer = Ethereum.Payload.Transfer.newBuilder().apply {
amount = ByteString.copyFrom("0x0de0b6b3a7640000".toHexByteArray())
}.build()
}.build()
}

val output = AnySigner.sign(signingInput.build(), ETHEREUM, SigningOutput.parser())
Expand All @@ -42,6 +46,63 @@ class TestEthereumTransactionSigner {
assertEquals(Numeric.toHexString(encoded), "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83")
}

@Test
fun testEthereumERC20Signing() {
val signingInput = Ethereum.SigningInput.newBuilder()
signingInput.apply {
privateKey = ByteString.copyFrom(PrivateKey("0x608dcb1742bb3fb7aec002074e3420e4fab7d00cced79ccdac53ed5b27138151".toHexByteArray()).data())
toAddress = "0x6b175474e89094c44da98b954eedeac495271d0f" // DAI
chainId = ByteString.copyFrom("0x1".toHexByteArray())
nonce = ByteString.copyFrom("0x0".toHexByteArray())
gasPrice = ByteString.copyFrom("0x09c7652400".toHexByteArray())
gasLimit = ByteString.copyFrom("0x0130B9".toHexByteArray())
payload = Ethereum.Payload.newBuilder().apply {
payloadErc20 = Ethereum.Payload.ERC20Transfer.newBuilder().apply {
to = "0x5322b34c88ed0691971bf52a7047448f0f4efc84"
amount = ByteString.copyFrom("0x1bc16d674ec80000".toHexByteArray())
}.build()
}.build()
}

val output = AnySigner.sign(signingInput.build(), ETHEREUM, SigningOutput.parser())
val encoded = AnySigner.encode(signingInput.build(), ETHEREUM)

assertArrayEquals(output.encoded.toByteArray(), encoded)
assertEquals(Numeric.toHexString(output.v.toByteArray()), "0x25")
assertEquals(Numeric.toHexString(output.r.toByteArray()), "0x724c62ad4fbf47346b02de06e603e013f26f26b56fdc0be7ba3d6273401d98ce")
assertEquals(Numeric.toHexString(output.s.toByteArray()), "0x032131cae15da7ddcda66963e8bef51ca0d9962bfef0547d3f02597a4a58c931")
assertEquals(Numeric.toHexString(encoded), "0xf8aa808509c7652400830130b9946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb0000000000000000000000005322b34c88ed0691971bf52a7047448f0f4efc840000000000000000000000000000000000000000000000001bc16d674ec8000025a0724c62ad4fbf47346b02de06e603e013f26f26b56fdc0be7ba3d6273401d98cea0032131cae15da7ddcda66963e8bef51ca0d9962bfef0547d3f02597a4a58c931")
}

@Test
fun testEthereumERC721Signing() {
val signingInput = Ethereum.SigningInput.newBuilder()
signingInput.apply {
privateKey = ByteString.copyFrom(PrivateKey("0x608dcb1742bb3fb7aec002074e3420e4fab7d00cced79ccdac53ed5b27138151".toHexByteArray()).data())
toAddress = "0x6b175474e89094c44da98b954eedeac495271d0f" // DAI
chainId = ByteString.copyFrom("0x1".toHexByteArray())
nonce = ByteString.copyFrom("0x0".toHexByteArray())
gasPrice = ByteString.copyFrom("0x09c7652400".toHexByteArray())
gasLimit = ByteString.copyFrom("0x0130B9".toHexByteArray())
payload = Ethereum.Payload.newBuilder().apply {
payloadErc721 = Ethereum.Payload.ERC721Transfer.newBuilder().apply {
from = "0x718046867b5b1782379a14eA4fc0c9b724DA94Fc"
to = "0x5322b34c88ed0691971bf52a7047448f0f4efc84"
tokenId = ByteString.copyFrom("0x23c47ee5".toHexByteArray())
}.build()
}.build()
}

val output = AnySigner.sign(signingInput.build(), ETHEREUM, SigningOutput.parser())
val encoded = AnySigner.encode(signingInput.build(), ETHEREUM)

assertArrayEquals(output.encoded.toByteArray(), encoded)
assertEquals(Numeric.toHexString(output.v.toByteArray()), "0x26")
assertEquals(Numeric.toHexString(output.r.toByteArray()), "0x4f35575c8dc6d0c12fd1ae0007a1395f2baa992d5d498f5ee381cdb7d46ed43c")
assertEquals(Numeric.toHexString(output.s.toByteArray()), "0x0935b9ceb724ab73806e7f43da6a3079e7404e2dc28fe030fef96cd13779ac04")
assertEquals(Numeric.toHexString(encoded), "0xf8b6808509c7652400830130b98080b86423b872dd000000000000000000000000718046867b5b1782379a14ea4fc0c9b724da94fc0000000000000000000000005322b34c88ed0691971bf52a7047448f0f4efc840000000000000000000000000000000000000000000000000000000023c47ee526a04f35575c8dc6d0c12fd1ae0007a1395f2baa992d5d498f5ee381cdb7d46ed43ca00935b9ceb724ab73806e7f43da6a3079e7404e2dc28fe030fef96cd13779ac04")
}

@Test
fun testSignJSON() {
val json = """
Expand All @@ -50,7 +111,11 @@ class TestEthereumTransactionSigner {
"gasPrice": "1pOkAA==",
"gasLimit": "Ugg=",
"toAddress": "0x7d8bf18C7cE84b3E175b339c4Ca93aEd1dD166F1",
"amount": "A0i8paFgAA=="
"payload": {
"payload_transfer": {
"amount":"A0i8paFgAA=="
}
}
}
"""
val key = "17209af590a86462395d5881e60d11c7fa7d482cfb02b5a01b93c2eeef243543".toHexByteArray()
Expand Down
110 changes: 83 additions & 27 deletions src/Ethereum/Signer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,31 @@ using namespace TW;
using namespace TW::Ethereum;

Proto::SigningOutput Signer::sign(const Proto::SigningInput& input) noexcept {
auto signer = Signer(load(input.chain_id()));
auto key = PrivateKey(Data(input.private_key().begin(), input.private_key().end()));
auto transaction = Signer::build(input);
try {
auto signer = Signer(load(input.chain_id()));
auto key = PrivateKey(Data(input.private_key().begin(), input.private_key().end()));
auto transaction = Signer::build(input);

signer.sign(key, transaction);
signer.sign(key, transaction);

auto output = Proto::SigningOutput();
auto output = Proto::SigningOutput();

auto encoded = RLP::encode(transaction);
output.set_encoded(encoded.data(), encoded.size());
auto encoded = RLP::encode(transaction);
output.set_encoded(encoded.data(), encoded.size());

auto v = store(transaction.v);
output.set_v(v.data(), v.size());
auto v = store(transaction.v);
output.set_v(v.data(), v.size());

auto r = store(transaction.r);
output.set_r(r.data(), r.size());
auto r = store(transaction.r);
output.set_r(r.data(), r.size());

auto s = store(transaction.s);
output.set_s(s.data(), s.size());
auto s = store(transaction.s);
output.set_s(s.data(), s.size());

return output;
return output;
} catch (std::exception&) {
return Proto::SigningOutput();
}
}

std::string Signer::signJSON(const std::string& json, const Data& key) {
Expand Down Expand Up @@ -67,21 +71,73 @@ Signer::sign(const uint256_t &chainID, const PrivateKey &privateKey, const Data&
return values(chainID, signature);
}

// May throw
Data addressStringToData(const std::string& asString) {
if (asString.empty()) {
return {};
}
auto address = Address(asString);
Data asData;
asData.resize(20);
std::copy(address.bytes.begin(), address.bytes.end(), asData.data());
return asData;
}

Transaction Signer::build(const Proto::SigningInput &input) {
Data toAddress;
if (!input.to_address().empty()) {
toAddress.resize(20);
auto address = Address(input.to_address());
std::copy(address.bytes.begin(), address.bytes.end(), toAddress.data());
Data toAddress = addressStringToData(input.to_address());
switch (input.payload().payload_oneof_case()) {
case Proto::Payload::kPayloadTransfer:
{
auto transaction = Transaction::buildTransfer(
/* nonce: */ load(input.nonce()),
/* gasPrice: */ load(input.gas_price()),
/* gasLimit: */ load(input.gas_limit()),
/* to: */ toAddress,
/* amount: */ load(input.payload().payload_transfer().amount()),
/* optionalPayload: */ Data(input.payload().payload_contract_generic().payload().begin(), input.payload().payload_contract_generic().payload().end()));
return transaction;
}

case Proto::Payload::kPayloadErc20:
{
Data tokenToAddress = addressStringToData(input.payload().payload_erc20().to());
auto transaction = Transaction::buildERC20Transfer(
/* nonce: */ load(input.nonce()),
/* gasPrice: */ load(input.gas_price()),
/* gasLimit: */ load(input.gas_limit()),
/* tokenContract: */ toAddress,
/* toAddress */ tokenToAddress,
/* amount: */ load(input.payload().payload_erc20().amount()));
return transaction;
}

case Proto::Payload::kPayloadErc721:
{
Data tokenToAddress = addressStringToData(input.payload().payload_erc721().to());
Data tokenFromAddress = addressStringToData(input.payload().payload_erc721().from());
auto transaction = Transaction::buildERC721Transfer(
/* nonce: */ load(input.nonce()),
/* gasPrice: */ load(input.gas_price()),
/* gasLimit: */ load(input.gas_limit()),
/* tokenContract: */ toAddress,
/* fromAddress: */ tokenFromAddress,
/* toAddress */ tokenToAddress,
/* tokenId: */ load(input.payload().payload_erc721().token_id()));
return transaction;
}

case Proto::Payload::kPayloadContractGeneric:
default:
{
auto transaction = Transaction::buildSmartContract(
/* nonce: */ load(input.nonce()),
/* gasPrice: */ load(input.gas_price()),
/* gasLimit: */ load(input.gas_limit()),
/* to: */ toAddress,
/* payload: */ Data(input.payload().payload_contract_generic().payload().begin(), input.payload().payload_contract_generic().payload().end()));
return transaction;
}
}
auto transaction = Transaction(
/* nonce: */ load(input.nonce()),
/* gasPrice: */ load(input.gas_price()),
/* gasLimit: */ load(input.gas_limit()),
/* to: */ toAddress,
/* amount: */ load(input.amount()),
/* payload: */ Data(input.payload().begin(), input.payload().end()));
return transaction;
}

void Signer::sign(const PrivateKey &privateKey, Transaction &transaction) const noexcept {
Expand Down
36 changes: 36 additions & 0 deletions src/Ethereum/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,41 @@
// file LICENSE at the root of the source code distribution tree.

#include "Transaction.h"
#include "ABI/Function.h"
#include "ABI/ParamBase.h"
#include "ABI/ParamAddress.h"

using namespace TW::Ethereum::ABI;
using namespace TW::Ethereum;
using namespace TW;

Transaction Transaction::buildERC20Transfer(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit,
const Data& tokenContract, const Data& toAddress, uint256_t amount) {
return Transaction(nonce, gasPrice, gasLimit, tokenContract, 0, buildERC20Call(toAddress, amount));
}

Transaction Transaction::buildERC721Transfer(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit,
const Data& tokenContract, const Data& from, const Data& to, uint256_t tokenId) {
return Transaction(nonce, gasPrice, gasLimit, {}, 0, buildERC721TransferFromCall(from, to, tokenId));
}

Data Transaction::buildERC20Call(const Data& to, uint256_t amount) {
auto func = Function("transfer", std::vector<std::shared_ptr<ParamBase>>{
std::make_shared<ParamAddress>(to),
std::make_shared<ParamUInt256>(amount)
});
Data payload;
func.encode(payload);
return payload;
}

Data Transaction::buildERC721TransferFromCall(const Data& from, const Data& to, uint256_t tokenId) {
auto func = Function("transferFrom", std::vector<std::shared_ptr<ParamBase>>{
std::make_shared<ParamAddress>(from),
std::make_shared<ParamAddress>(to),
std::make_shared<ParamUInt256>(tokenId)
});
Data payload;
func.encode(payload);
return payload;
}
29 changes: 26 additions & 3 deletions src/Ethereum/Transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
namespace TW::Ethereum {

class Transaction {
public:
public:
uint256_t nonce;
uint256_t gasPrice;
uint256_t gasLimit;
Expand All @@ -26,8 +26,31 @@ class Transaction {
uint256_t r = uint256_t();
uint256_t s = uint256_t();

Transaction(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit, const Data& to, uint256_t amount,
Data payload)
// Factory methods
// Create a native coin transfer transaction
static Transaction buildTransfer(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit, const Data& to, uint256_t amount, const Data& optionalPayload = {}) {
return Transaction(nonce, gasPrice, gasLimit, to, amount, optionalPayload);
}

// Create an ERC20 token transfer transaction
static Transaction buildERC20Transfer(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit,
const Data& tokenContract, const Data& toAddress, uint256_t amount);

// Create an ERC721 NFT transfer transaction
static Transaction buildERC721Transfer(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit,
const Data& tokenContract, const Data& from, const Data& to, uint256_t tokenId);

// Create a generic smart contract transaction
static Transaction buildSmartContract(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit, const Data& to, const Data& payload) {
return Transaction(nonce, gasPrice, gasLimit, to, 0, payload);
}

// Helpers for building contract calls
static Data buildERC20Call(const Data& to, uint256_t amount);
static Data buildERC721TransferFromCall(const Data& from, const Data& to, uint256_t tokenId);

private:
Transaction(uint256_t nonce, uint256_t gasPrice, uint256_t gasLimit, const Data& to, uint256_t amount, const Data& payload)
: nonce(std::move(nonce))
, gasPrice(std::move(gasPrice))
, gasLimit(std::move(gasLimit))
Expand Down
2 changes: 1 addition & 1 deletion src/Solana/Transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ using namespace std;
uint8_t CompiledInstruction::findAccount(const Address& address) {
auto it = std::find(addresses.begin(), addresses.end(), address);
if (it == addresses.end()) {
throw new std::invalid_argument("address not found");
throw std::invalid_argument("address not found");
}
assert(it != addresses.end());
auto dist = std::distance(addresses.begin(), it);
Expand Down
53 changes: 46 additions & 7 deletions src/proto/Ethereum.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,49 @@ syntax = "proto3";
package TW.Ethereum.Proto;
option java_package = "wallet.core.jni.proto";

// Transaction payload (transfer, smart contract call, ...)
message Payload {
optout21 marked this conversation as resolved.
Show resolved Hide resolved
// Native coin transfer transaction
message Transfer {
// Amount to send in wei (256-bit number)
bytes amount = 1;

// Optional payload
bytes payload = 2;
optout21 marked this conversation as resolved.
Show resolved Hide resolved
}

// ERC20 token transfer transaction
message ERC20Transfer {
string to = 1;

// Amount to send (256-bit number)
bytes amount = 2;
}

// ERC721 NFT transfer transaction
message ERC721Transfer {
string from = 1;

string to = 2;

// ID of the token (256-bit number)
bytes token_id = 3;
}

// Generic smart contract transaction
message ContractGeneric {
// Contract call payload
bytes payload = 1;
optout21 marked this conversation as resolved.
Show resolved Hide resolved
}

oneof payload_oneof {
Transfer payload_transfer = 1;
ERC20Transfer payload_erc20 = 2;
optout21 marked this conversation as resolved.
Show resolved Hide resolved
ERC721Transfer payload_erc721 = 3;
ContractGeneric payload_contract_generic = 4;
}
}

// Input data necessary to create a signed transaction.
message SigningInput {
// Chain identifier (256-bit number)
Expand All @@ -20,14 +63,10 @@ message SigningInput {
// Recipient's address.
string to_address = 5;

// Amount to send in wei (256-bit number)
bytes amount = 6;

// Optional payload
bytes payload = 7;

// Private key.
bytes private_key = 8;
bytes private_key = 6;

Payload payload = 7;
}

// Transaction signing output.
Expand Down
Loading