# Using Ethereum wallets on TON 💎

By [dmitrybase](https://paragraph.com/@dmitrybase) · 2024-10-28

---

**_Or How to handle ECDSA signatures in TON_**

**TLDR**
========

> This post explains how to sign and verify data and transactions in TON using **ECDSA** (though TON primarily uses EDDSA). It assumes familiarity with EVM and basic TON concepts)

Keep in mind, this is my very first post. Cheers.

* * *

Hey, it's [@swift1337](https://github.com/swift1337)! I do protocol @ [ZetaChain](https://www.zetachain.com/) 🐲

Being a protocol engineer is fun. It involves solving specific, niche challenges every day. At ZetaChain we're are working hard to bring omnichain capabilities to The Open Network ([TON](https://ton.org/)) 💎

During this journey, one of the interesting technical challenges was signing TON transactions using ZetaChain's Threshold Signature Schemes (**TSS**) protocol, which relies on cryptography that differs from what is used in TON. The main use case is to bridge and withdraw locked TON during cross-chain transactions, with support of calling arbitrary smart contracts on connected chains.

In this post, I want to elaborate on how we solved ECDSA on TON and enabled support for Multi-Party Computation (MPC).

Other use cases may include using **Metamask for signing ton transactions**, as well as cross-chain governance.

All of the sources are available in our repo:

[https://github.com/zeta-chain/protocol-contracts-ton](https://github.com/zeta-chain/protocol-contracts-ton)

On Curves and Signatures
------------------------

![Facts](https://storage.googleapis.com/papyrus_images/62b61ce55049f6c2f7fc8dba09c1e2a461103a8561136279b19b2d6b4fcce147.png)

Facts

Bitcoin, Ethereum, and many other blockchains use the Elliptic Curve Digital Signature Algorithm ([ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)) and specifically the `secp256k1` curve to sign and verify data. TON, on the other hand, uses the Edwards-curve Digital Signature Algorithm ([EdDSA](https://en.wikipedia.org/wiki/EdDSA)) with  `ed25519` curve.

### What is a cryptographic signature?

Basically, it's a mathematical way of proving that a message came from a specific signer.

For example, we can make a signature of "Alice says `Hello, Bob!`". Technically, it would work like this:

    Signer: Alice (0x0fA03A88042647025f8524b12899c6A3f1781A2e)
    Input: "Hello, Bob!"
    
    Signature:
    0x07965b9c69767e2b45fe086aebc4fc103baf26f27f6176e9eb6a3458743ceef877d4e04987619321c7bcf5a2311d25fa61e48ac68c388b4d45bf42f098de831c1b
    

**Magic 🧙🏻‍♂️ This is literally the backbone of any blockchain**

The big brains of cryptography (not meme coins) 🧠 realized that it's inconvenient to operate with raw messages of arbitrary size (imagine signing a large book). To keep signatures consistent, we sign the hash of the message (usually SHA-256) instead of the entire message.

A process of getting a public key from a signature is called **signature recovery**. Technically, it looks like this:

    // pseudo code
    alicePubKey = ecdsa_recover(sha256("Hello, Bob!"), signature)
    
    alice = pubKeyToAddress(alicePubKey) // 0x0fA03A88042647025f85....
    

Regarding the structure of this signature, it has **65 bytes**:

*   32 bytes for **R**: One of the elliptic curve point coordinates, linking the message to the signature.
    
*   32 bytes for **S**: Derived from the message, private key, and r value. Both r and s are needed to verify the signature.
    
*   1 byte for **V**: A recovery identifier (0 or 1) used to determine the correct public key for signature verification.
    

If you want to have an overview the topic, I suggest these introductory sources:

*   [https://learnmeabitcoin.com/technical/cryptography/elliptic-curve/ecdsa/](https://learnmeabitcoin.com/technical/cryptography/elliptic-curve/ecdsa/)
    
*   [https://habr.com/ru/articles/692072/](https://habr.com/ru/articles/692072/)
    
*     
    
*     
    

TON of Quirks and Superiority
-----------------------------

TON is _far_ superior to ETH in technical terms ([link](https://ton.org/comparison_of_blockchains.pdf)). Because `async` and `sharding` are badass words for a reason! Just acknowledge it, comrade. `/s`

[Very Interesting](https://www.quora.com/What-does-it-mean-when-the-British-say-very-interesting), but a regular wallet is also a smart contract. Due to this, even a simple coin transfer is actually two transactions ("Alice sends a message with coins to Bob" + "Bob receives a message with coins from Alice").

![avg TON wallet transaction](https://storage.googleapis.com/papyrus_images/95d1a5b86a8fa5135c4f3ef3d79bffc8245cac8ed6b9f88c58a2c42874b2a8fa.png)

avg TON wallet transaction

When Alice sends 1 TON to Bob, Alice creates and signs a message off-chain and then broadcasts it to Alice's smart contract on TON. As of Q4 2024, the latest wallet implementation is [WalletV5](https://github.com/ton-blockchain/wallet-contract-v5). It uses the following message structure for handling an external message (`recv_external`):

    // pseudocode
    cell msg_body: $someData + $signature
    

Okay, forget pseudo code, we're not pseudo nerds! Warm up your fingers, let's see some _superiority_ in practice:

[https://github.com/ton-blockchain/wallet-contract-v5/blob/main/contracts/wallet\_v5.fc](https://github.com/ton-blockchain/wallet-contract-v5/blob/main/contracts/wallet_v5.fc)

### How Wallet V5 checks EDDSA signatures

    () process_signed_request(slice in_msg_body, int is_external) impure inline {
      slice signature = in_msg_body.get_last_bits(size::signature);
      slice signed_slice = in_msg_body.remove_last_bits(size::signature);
    
      ;; ...
      
      slice data_slice = get_data().begin_parse();
      
      ;; The wallet fetches Alice's public key from its state
      ;; It's stored when Alice deploys her wallet
      ;; This is usually done automatically by most wallets (e.g. TonKeeper)
      ;; during the first transaction
      int public_key = data_slice~load_uint(size::public_key);
    
      ;; ...
    
      int is_signature_valid = check_signature(slice_hash(signed_slice), signature, public_key);
    
      ;; further wallet's logic ......
    }
    

As you can see, the logic is pretty straightforward:

*   Split the message into `signed_slice` (payload) and `signature`
    
*   Hash the payload: `slice_hash(signed_slice)`
    
*   Take the owner's public key from the contract's state
    
*   Call `check_signature(hash, signature, public_key)`
    

And `check_signature` is actually an alias from `stdlib.fc`:

    ;; EDDSA signature check
    int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU";
    

You can read more about TVM instructions here

[https://docs.ton.org/learn/tvm-instructions/instructions](https://docs.ton.org/learn/tvm-instructions/instructions)

The Key Point
-------------

To implement a contract or a wallet in TON that can send assets and invoke operations based on an "EVM signer", we need to have a way to recover an ECDSA signature. Actually, there is a dedicated TVM instruction for that! Meet `ECRECOVER`:

> Recovers public key from signature, identical to Bitcoin/Ethereum operations. Takes 32-byte hash as uint256 `hash`; 65-byte signature as uint8 `v` and uint256 `r`, `s`. Returns `0` on failure, public key and `-1` on success. The 65-byte public key is returned as uint8 `h`, uint256 `x1`, `x2`.

Show Me the Code
----------------

Here's the flow that we'll use:

1.  Encode TON external message as bytes
    
2.  Sign it using ECDSA
    
3.  Prepend/Append it to the message
    
4.  Check the signature using `ECRECOVER`
    
5.  If the signature matches the expected sender, invoke the operation (e.g., send TON coins)
    

Let's say we want to implement a custody contract that allows us to send TON based on an ECDSA signature signed from Metamask. Then we can use the following external message body:

    cell
      > signature: 65 bytes -> slice of 520 bits
      > cell_ref:
        > cell: arbitrary payload
    

If you think “wtf is a cell?!”, you can read [this](https://docs.ton.org/learn/overviews/cells).

As an example, I'll use ZetaChain's cross-chain Gateway implementation.

    cell auth::ecdsa::external(slice message, slice expected_evm_address) inline {
      ;; 1. Get signature
      slice signature = message~load_bits(size::signature_size);
    
      ;; 2. Get payload cell
      throw_if(error::no_signed_payload, message.slice_refs_empty?());
      cell payload = message~load_ref();
    
      ;; 3. Calculate payload hash
      int payload_hash = cell_hash(payload);
    
      ;; 4. Check signature
      int sig_check = check_ecdsa_signature(payload_hash, signature, expected_evm_address);
    
      if (sig_check != true) {
          ~strdump("check_ecdsa_signature");
          sig_check~dump();
          throw(error::invalid_signature);
      }
    
      return payload;
    }
    

**crypto.fc**

    ;; Returns keccak256 hash of the data as uint256
    int hash_keccak256(builder b) asm "1 INT HASHEXT_KECCAK256";
    
    ;; ECRECOVER FunC wrapper
    (int, int, int, int) ecdsa_recover(int hash, int v, int r, int s) asm "ECRECOVER";
    
    ;; TVM uses `v` ONLY as `0` or `1`. In ETH/BTC, a prefix is used. See RFC6979
    ;; See https://bitcoin.stackexchange.com/questions/38351/ecdsa-v-r-s-what-is-v
    int normalize_ecdsa_recovery_id(int v) inline {
      ;; "compressed recovery_id for pub key"
      if v >= 31 {
          return v - 31;
      }
    
      ;; "uncompressed recovery_id for pub key"
      if v >= 27 {
          return v - 27;
      }
    
      return v;
    }
    
    ;; Checks ECDSA signature. Returns int as an outcome:
    ;; 1: unable to recover public key
    ;; 2: recovered key is not uncompressed
    ;; 3: recovered address does not match the expected address
    ;; -1 (true): signature is valid
    (int) check_ecdsa_signature(int hash, slice signature, slice expected_evm_address) impure inline_ref {
      ;; 1 Parse (v, r, s)
      int v = signature~load_uint(8).normalize_ecdsa_recovery_id();
      int r = signature~load_uint(256);
      int s = signature~load_uint(256);
    
      ;; 2. Recover public key
      (int h, int x1, int x2, int flag) = ecdsa_recover(hash, v, r, s);
      if flag != true {
          return 1;
      }
    
      ;; Deny compressed public keys (0x04 prefix means uncompressed)
      if h != 4 {
          return 2;
      }
    
      ;; 3. Derive 20 bytes evm address from the public key
      int pub_key_hash = begin_cell()
          .store_uint(x1, 256)
          .store_uint(x2, 256)
          .hash_keccak256();
    
      slice actual_evm_address = begin_cell()
          .store_uint(pub_key_hash, 256)
          .end_cell()
          .begin_parse()
          .slice_last(20 * 8);
    
      ;; 4. Compare with the expected address
      if equal_slices(expected_evm_address, actual_evm_address) == false {
          return 3;
      }
    
      return true;
    }
    

And that’s a wrap. Congrats for reading this. ECDSA on TON — because why settle for one chain when you can bridge them all?

---

*Originally published on [dmitrybase](https://paragraph.com/@dmitrybase/using-ethereum-wallets-on-ton)*
