Ethereum Griefing Wallets: Send w/Throw Is Dangerous

Using send to send money to a nasty ethereum wallet can lock your contract if implemented improperly, and there is no public information about this, nor are there currently public instructions about how to remediate this risk.

Even worse, current best practices recommendations exacerbate this risk, rather than mitigate it.

TL;DR -- If you think you don't need to worry about send in Ethereum because you throw when there's a problem, you are very, very wrong. Don't throw on a failed send. Instead, return the remaining balance less any gas consumed during execution of the contract.

Part 1 of the Problem: Sends Can Fail

Let's check out the send function's signature in solidity:

<address>.send(uint256) returns (bool)  

The function takes an amount, sends it to an address, and returns true if it sent successfully or false if the send failed.

As of this writing, no official example code for solidity checks the results of a failed send.

It's apparently so common to think that sends work unconditionally that it is the only behavior listed in the "pitfalls" section of the Ethereum Solidity documentation. One simple reason for that might be because the example code doesn't lead by example.

Obviously, if you're trying to keep track of balances, perhaps on behalf of customers, in a smart contract, a failed send that you didn't expect is going to be a large pain in the ass.

For a stellar and salacious tale of this problem in the real world, I highly recommend the King of Ether Post-mortem. My favorite line:

Do not develop Solidity contracts without a reasonable grasp of the underlying Ethereum Virtual Machine execution model, particularly around gas costs.

That should give you a bit of pause, it certainly gives me pause, and I'm a fairly experienced engineer with world-class blockchain experience.

Why Would A Send Fail?

The most interesting reason is because there's not enough gas to execute the default function at the receiving contract.

If you come from Bitcoin or other Bitcoin-inspired cryptocurrencies, you're going to want to read that again. It surprised me to learn it when Redditor ItsAConspiracy explained it to me.

Here's the deal. In Bitcoin, an address is the public key that corresponds to a private key held by a wallet. I'm lying a bit to aid comprehension. But, fundamentally, it's just a bit of data.

In Ethereum, an address could be similar -- not a contract, but a public key.

But, it could also be another smart contract's address. The Mist client encourages users to make a wallet contract as a first step after loading up, for example. Users would then offer the address of that contract as their 'wallet address'.

Other contracts do not typically utilize any mechanism to distinguish between an address of a private key, and an address of a wallet.

It's a fundamental transaction in Ethereum to send money to a contract, and developers seem to expect it to 'just work' like in Bitcoin or other digital currencies, perhaps with a transaction fee attached.

Thanks For Sending Money, What Do I Do With It?

What does a contract do when it receives money, but not extradata or function calls along with the cash? The default function is called.

Most ethereum contracts will define a default function like this:

function() {  
   // If this contract doesn't accept money, we can reverse the send by calling
   throw; // everything about this execution of the  contract is now unwound, and all gas is burned
  // or 
   msg.sender.send(msg.amount); // if we'd never called throw, this would return the money and keep any remaining gas.
}

But with this send we are now calling a function in another contract. Ethereum charges us for that, as it should. It charges gas. (The gas cost of a contract execution includes memory usage, computer cycles and other things.)

The caller of a function pays the gas in the Ethereum model. This makes sense -- we want to use the contract or one of its functions -- so, we should pay.

How much gas do we pay for a send? If you call send, the contract will offer up 23,000 gas as available funds for the default function at the receiving contract.

A standard recommendation for the smart Ethereum Developer is to write code like this:

if (!(address.send(amount))) throw;  

The idea being that if we have trouble sending ether somewhere, we're going to notice it, and stop the program from continuing -- it might be harmful to keep going believing we've sent out funds when we haven't.

This idea is a nice first step try at mitigating problems with sends. It's the recommended best practices right now. Do Not Do This. Following it could put your contract at risk.

Griefing

To recap:

  1. Most contracts don't check for send success.
  2. Most contracts only offer 23,000 gas for a send.
  3. All contracts that send must pay enough gas for successful execution of the default function.
  4. Best practices recommend contracts call throw for a failed send.

The attack: construct a wallet with a very expensive set of operations in the default function and use that while engaging with smart contracts you wish to disrupt. Voila.

Example

This sounds esoteric. It's actually a very common problem for existing smart contracts. Here's some sample code for a DAO that is vulnerable to griefing like this.

This mythical DAO has a certain number of investors, and is at capacity. If a new investor comes along and offers more money than the smallest investor, the DAO will dividend out to the smallest, and put the new one in, increasing its capital stake.

Bad "Best Practices" Code Follows: Do Not Do This

for (uint i=0; i<investors.length; i++) {  
  if (investors[i].invested == min_investment) {
    // Refund, and check for failure. 
    // This code looks benign but will lock the entire contract
    // if attacked by a griefing wallet.
    if (!(investors[i].address.send(investors[i].dividendAmount))) 
      { 
        throw;
      }
    investors[i] = newInvestor;
  }
}

A griefing wallet with a default function that takes, say, one million gas to run, can lock this function completely by merely investing enough to become the smallest investor.

When the griefing wallet is next due to be booted off the investor list, the contract will try and return money to the contract, and then it will fail, and throw, and revert all changes.

If this sort of logic is in your default function, or your selfdestruct function, you have a real problem: getting a griefing wallet address into your contract's storage may cause loss of capital or control of your contract.

Recommendation

  1. It's tempting to use throw on a bad send. In general, don't. Instead, trap and deal with failed sends by calculating out how much can or should be returned to the sender, and sending it.

  2. Triple check your bookkeeping logic around failed sends -- you will need to account for the gas spent in case of failure. Yes, this is harder than calling throw. But, it also won't melt your contract down and leave it unrecoverable.

  3. Anywhere that deals with user-provided addresses should be inspected thoroughly with the question "What if I could never send money to this address?" in mind.

If you'd like an official audit, I am available to select clients. Contact me at [email protected].

Peter Vessenes

Read more posts by this author.

Subscribe to Peter Vessenes

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!