The EVM’s execute-on-transfer problem and non-payable contracts

Ashley Houston
Qtum
Published in
6 min readNov 2, 2018

--

This week (month/year?) in EVM design annoyance posts, I want to talk about the false concept the EVM and Solidity gives for “non-payable” contracts and one of the bigger attack vectors for smart contracts that is near impossible to fix. For reference, the EVM is what makes Ethereum, Qtum, and probably a few other blockchain contracts work. Basically if you’re writing Solidity, you’re using the EVM. For this article, I’ll just assume it’s all about Ethereum and use ETH as the main-chain currency. This equally applies when using QTUM as the main-chain currency or any other.

For the uninitiated, the EVM doesn’t really expose a general purpose interface for transferring Ethereum without potentially executing some contract code (and thus the transfer failing/throwing an exception)… however, there do exist more difficult to use ways of forcing a contract to accept ETH.

Unexpected ETH received attack vector

Unexpected balance changes don’t affect all contracts. Some of course will just have a surplus of ETH that can never be accessed. However, some contracts can be completely broken by this aspect and thus be required to keep an expectedBalance state variable or redesigned to handle unexpected balance changes.

A contrived example is of course something like this:

pragma solidity 0.4.18;
contract ForceEther {
bool youWin = false;
function onlyNonZeroBalance() {
require(this.balance > 0);
youWin = true;
}
// throw if any ether is received
function() payable {
revert();
}
}

This is a slightly more realistic example:

contract EtherGame { 
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
mapping(address => uint) redeemableEther;
// users pay 0.5 ether. At specific milestones, credit their accounts
function play() public payable {
require(msg.value == 0.5 ether); // each play is 0.5 ether
uint currentBalance = this.balance + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
// if at a milestone credit the players account
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
return;
}
function claimReward() public {
// ensure the game is complete
require(this.balance == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
redeemableEther[msg.sender] = 0;
msg.sender.transfer(redeemableEther[msg.sender]);
}
}

In this, the idea is that users send the contract 0.5 ETH increments to try to reach each milestone. An attacker could forcibly send ETH to this contract to push it over the finalMileStone value, and thus make all funds in the contract locked and inaccessible. Of course, this is also pretty contrived.

Unfortunately I can’t find any real-life examples of this being exploited in the wild yet. However, these types of exploits can be extremely devious when purposefully crafted to stay hidden in a Solidity contract. Attacks of this nature were key elements in both second and third place winners of the Underhanded Solidity contest.

Forcing ETH into a contract

First, let’s cover how transfers typically work. So, lets say you have this code:

someAddress.transfer(self.balance() / 10);

What does this code do? Well, here’s the options:

  1. someAddress could be a non-contract address, in which case no contract execution occurs and no gas is consumed above the standard transfer fee. This can never fail, but there is also no guarantee that the address is real and accessible
  2. someAddress could be a contract address. The contract can succeed and will be provided with enough gas to log and event and pretty much nothing else
  3. someAddress could be a contract address. The contract can fail (ie, non-payable) and the money will be returned, all gas consumed, and the execution of the contract calling the transfer stopped.

So this seems ok. You’d want the transfer to fail if a contract isn’t wanting to receive ETH, or potentially to whitelist only certain people/contracts being capable of sending ETH to the contract. However, the use case of this as a security feature is completely broken since contracts can be sent ETH in other ways:

  • A contract can be created and sent ETH. This contract can then selfdestruct, sending it’s ETH to the vulnerable contract without calling any of the vulnerable contract’s code
  • If the sender and nonce of a vulnerable contract can be predicted before it’s deployment, then ETH can be sent to that address beforehand.
  • Miners/block creators can reward ETH to the contract directly without any contract execution

None of these methods are suitable for general use, but they do expose a way to send ETH to a contract and completely side-step any potentially code preventing it.

The only easy method of exploiting this without special privilege is to use the self-destruct method. This only requires the ETH you need to exploit the contract, and some extra gas fees.

The other interesting method is sending ETH to a contract before it’s been created. This can be used by a malicious contract developer to utilize a subtle exploit that is purposefully placed in the code. These kind of bugs with contract balance can be hard to detect, and no auditing software I’ve seen is capable of pointing them out beyond obvious stuff like require(balance == whatever).

To really prevent something like this, we can use this code:

uint256 expectedBalance = 0;
function () public payable {
expectedBalance+=msg.value;
}

Alright, easy enough problem to work around? Now we just send it some ETH with .transfer() and

VM Exception while processing transaction: out of gas

Oh, this is significantly more expensive now because we now have to read and write a state variable. The default gas stipend is 2300 gas. To read a state variable it costs 200 gas, and to write one that already exists it costs 5000 gas (or 20,000 gas if it didn’t previously exist)

So, we need something that rather looks like this on the calling side:

someAddress.transfer.value(whatever).gas(7000); //just a guess

This is of course non-standard and thus most contracts when transferring value just use the default stipend… However, even over looking that problem, now that we have more gas required in our interface, we could potentially have a more interesting contract payable function also:

function () public payable{
sender.call(....);
}

So now to properly allow the contracts we call to mitigate the unexpected balance issue, we open up the contract to a new form of attack. The default stipend of 2300 is only enough to call a very thin external function that does no state modifying operations, and basically can only make a LOG. Now that we have to increase the gas stipend so that contracts can read and store state, the contract now also has enough gas to call the contract that called it, potentially setting up for a reentrancy attack. I won’t go into all the details, but basically it requires an incredibly careful contract design to prevent any bugs from this “feature”, and the only 100% way to avoid dealing with it is a gas stipend too low to make state modifications. There is no 100% way of knowing that the current contract execution is a reentrant one, the EVM does not expose that information.

Conclusion

To me, this portion of the EVM design makes no sense. There is approval required for accepting funds normally, which seems to be painted as a security feature, while there are clear and documented ways of getting around it, so it provides no value as far as security. Furthermore, if you want to spend less gas on fees for value transfers, then these methods are for the most part inaccessible to you, they’re really only useful for attackers. In theory, the gas cost for sending coins to a contract where you DON’T care about executing the contract should be 33% cheaper. Why does the EVM not expose some way of doing this? Historically this unexpected behavior has led to many contract DoS attacks and bugs. Fortunately Solidity has made it harder to have these types of bugs (ie, not checking send() result, wrong order of changing internal state vs sending coins, etc) but obviously it's not a completely solved problem for the security aspect, much less the economic one.

If you do want to keep track of your exact expected balance in a contract, then you need to pay even more gas than normal and use a non-standard interface (ie, not .transfer()), which actually introduces significantly more risk into your smart contract through reentrancy attacks.

I guess we’ll probably never know why the EVM was designed this way, but suffice to say it’s a problem we plan on fixing with Qtum’s x86 VM (though unfortunately it’s not easy to fix in Qtum’s EVM). There will be a method to send value to a contract without executing the contract, and yes, it’ll be cheaper than actually executing it.

--

--

(archived) Blockchain Engineer, co-founder at Qtum, President of Earl Grey Tech. All in on blockchain tech. Also does some film photography