De-anonymizing eosblender.com

Categories:

eosBlender, the first EOS mixing service, recently came to my intention by this EOS NY tweet:

I don’t want to go as far as saying that EOS NY is endorsing or affiliated with bloxchanger’s eosBlender mixing service in any way, but one should at least be more careful about promoting projects that have obvious red flags:

  • anonymous team
  • section on “How it works” doesn’t explain how it works

Fortunately, it was already called out in the twitter replies. I now had a deeper look at how it actually works. Big surprise, it’s useless and it’s easy to de-anonymize any transaction. But first, let’s have a look at the website:

eosBlender aims to make transactions safer and untraceable. It’s a service that mixes different streams of potentially identifiable EOSIO payments. This improves the anonymity of token transfers while contributing towards privacy over internet transactions.

  1. The sender deposits a given amount of EOS on the eosioblender smart contract and assigns the deposit to a specific receiver.
  2. The receiver can then activate a claim action for the same amount and receive the corresponding transfer from the smart contract.

The sender’s payment gets mixed with that of other users in the eosioblender account. The transaction data is not publicly disclosed. In fact, the name of the receiver and amount of the transfer are temporarily recorded in form of cryptographic hash values that only the smart contract can decrypt when the receiver claims the funds. The protocol eliminates the explicit link between the original transaction and the receiver address. At the same time, it maintains the same security properties of the EOSIO token transfers.

My highlights are ”temporarily recorded” and ”only the smart contract can decrypt“. Nothing can just be temporarily recorded on a blockchain, everything is permanent and public. Hashes can also not be ”decrypted”, you find an input/preimage that matches the hash - the terminology is wrong. It’s also obvious that if the smart contract can decrypt something, anyone can do the same by just running the WASM code with the action data.

What is eosBlender actually doing?

eosBlender works by sending two transactions.

  1. The first one sends X EOS to eosBlender and assigns it to a receiver. The receiver information is encoded in two hashes (depositId and myreceiverHash) and therefore not immediately visible. That’s all the privacy eosBlender provides.
  2. The receiver can now claim the funds by providing a third hash (controlhash).

Let’s have a look at example transactions from the bloxchanger team trying to hide their own transfers. (Looking at the volume of eosBlender, they are probably also the only ones using it.)

First, the EOS transfer & assign transaction from accountspawn account to eosioblender involves these hashes:

{
   "depositId": "aa0dcdc28a6a2cb2138b69bda66d0a267ee6996407fa2778840bfb43dc5c0df9",
   "myreceiverHash": "224591065c76b7ee3428d05f2a72bd8d29fc1bd36bc286ee874c959f977f85c6",
   "quantity": "1.0000 EOS",
   "username": "accountspawn"
}

Second, the claim transaction from the bloxchangers account:

{
   "controlhash": "7fb3b1b7bd2434ecc216db39d549694c077a1dedd5b9f4bea08d5ef3db3810d7",
   "myreceiver": "bloxchangers",
   "quantity": "1.0000 EOS"
}

The anonymity is based on the hope that the controlhash of the claim transaction is not linkable to any of the hashes of the transfer action. It takes about 10 minutes of reverse-engineering to see how these hashes are computed and can be linked. The frontend code is not even minimized and readable in plain:

// how the eosblender hashes are computed

// arguments are from the initial transfer transaction
// in our example receiver = `bloxchangers`, amount = `1`, memo = secret memo from frontend
const computeHashes = ({ receiver, memo, amount }) => {
  let checkcode = "c8PGNTDRuY3JX8tFJePGP1K7qktqRwBGv";
  let mydepostr =
    checkcode + memo + Number(amount).toFixed(4) + " EOS" + receiver;
  let depositId = sha256(mydepostr);

  let claimcode = "KVEE6xCu6UFgUzv6HoQhtn66HmTcoQa";

  let mystr = receiver + Number(amount).toFixed(4) + " EOS" + memo + checkcode;
  let controlhash = sha256(mystr);
  let myreceiverHash = sha256(controlhash + receiver + claimcode);

  return {
    depositId: depositId,
    myreceiverHash: myreceiverHash,
    controlhash,
  };
};

Given the transfer inputs from the web UI, one can compute all three hashes used in the transfer and claim transactions.

The memo used for the computation is not the memo of the eosio.token::transfer action. It’s a secret memo one inputs in the frontend that is not stored on-chain. The receiver needs to input this in the frontend as well when claiming the action. Therefore, it’s at least not possible for a third-party to claim your funds.

We can completely de-anonymize any eosBlender claim by doing the same calculations as above. I created a proof-of-concept app where one enters the receiver and controlhash of a claim, and the myreceiverhash is computed which uniquely identifies the funding transaction. You just need to search the chain for a transfer transaction with the corresponding myreceiverhash. The code is open-source on codesandbox:

In the end, security must be based solely on mathematics, and if projects are reluctant to show their code or explain the technical details of their protocols, run away. 🏃‍♂️ What we got is a security through obscurity mixing service that is completely useless - but hey, at least the smart contract code was locked for a couple of weeks!

Learn EOS Development Signup

Hi, I'm Christoph Michel 👋

I'm a , , and .

Currently, I mostly work in software security and do on an independent contractor basis.

I strive for efficiency and therefore track many aspects of my life.