Fallback functions and the fundamental limitations of using send() in Ethereum & Solidity

Arul edited this page Apr 17, 2018 · 4 revisions

(or: How to effectively send ether around)

Intro

Fallback functions are triggered when the function signature does not match any of the available functions in a Solidity contract. For example, if you do address.call(bytes4(bytes32(sha3("thisShouldBeAFunction(uint,bytes32)"))), 1, "test"), then the EVM will try to call "thisShouldBeAFunction" at that address. If it does not exist, then the fallback function is triggered.

send() specifies a blank function signature and thus it will always trigger the fallback function if it exists.

Here's an example (from: https://github.com/ethereum/wiki/wiki/Solidity-Tutorial#fallback-functions):

contract Test {
  function() { x = 1; }
  uint x;
}

contract Caller {
  function callTest(address testAddress) {
    Test(testAddress).call('0xabcdefgh'); // hash does not exist
    // results in Test(testAddress).x becoming == 1.
  }
}

Abuse?

Let's assume you have the following scenario:

You have a company running on Ethereum, and a dividend is to be paid (in Ether) to the owners. For simplicity, there is 100 owners. According to their portions, ether is divided up and sent to each of them. This costs a lot of gas, but is taken out of the revenue that's been made by the company. When using send(), the fallback function is triggered. There is no discern between a normal address and a contract address in the EVM. Most sends are working fine, until owner 11. Suddenly, all the gas gets gobbled up, and the transaction fails. No one else gets paid.

What happened here is that owner 11 implemented a malicious piece of code. He used a contract address and in its fallback function implemented an infinite loop. All the gas gets used and thus it is not possible to send any money without removing owner 11 as a shareholder.

Fix

This was a fundamental problem to unilaterally trusting other pieces of code on Ethereum, especially when using send(). This was fixed as follows:

  1. Send() does not forward gas anymore. It simply uses the hardcoded stipend (2300 gas) siphoned from the value transfer cost (minimum 9040). It's enough to send ether, but also enough to basically do one additional small logging operation. You can't do another value transfer (it costs minimum 9040 gas) and you can't do things like storing variables.
  2. If a send() call runs out of gas, it does not throw an error, it simply returns false().

Thus, you can now use send() safely in the above example. The malicious owner uses up its allotted gas stipend, and it returns false. All the other transactions simply continue and everyone gets paid properly.


Additional problems.

However, this paints a new problem.

What if a contract is supposed to do something when money is sent to it? Using send() + fallback function is limited. You can't create layered sends in this way. In other words, tx.origin sends ETH to contract A. And now contract A needs to forward money to contract B. And now contract B needs to forward money to contract C and so forth. This isn't possible. 2300 gas is sent with the stipend and does not have enough gas to even issue another send().

Workarounds?

address.call.value()();

There is a workaround. You must use call(), sending along ether, whilst doing so. call() does forward gas. So, to send ether automatically around, whilst still working like "send()", you would do the following call instead:

address.call.value()();

This sends wei to any address (normal or contract). If there is a fallback function, it is triggered as well. However, as you might realise by now, this opens up the possibility of the above vulnerability. A long loop will simply deplete the gas.

Something like will cost at least 50000000 gas:

 contract Gas_Loop {

    function() {
        for(uint i = 0; i < 10000; i+=1) {
            out_i = i;
        }
    }

    uint public out_i;
 }

In Solidity, issuing a call() without specifying the gas, it tries to save 34050 gas for the rest of the operations. The rest of the gas is forwarded along. If the sub-execution does not use all the gas, it is returned. If you specify the exact amount of gas, ie "address.gas(20000).value(1).call()();" it, of course just sends along that precise amount.

Specifying the gas cost per "value transfer" could work if you know beforehand how deep the layer is and how many value transfers will occur. This is difficult to determine though.

Breadth, not Depth

If the system relies on the fact that at each point, wei should be transferred, then instead of using fallback functions and multi-layered sub-executions, the system could first map the layers and then issue sends() from the top layer. ie, instead of doing "tx.origin -> send() -> send() -> send() -> send() -> etc", it will do:

                                    /-> send()
tx.origin -> doCrawlOfWhomToPay() -> send()
                                   \-> send()
                                    |-> send()

Code Audits

Another possible way to use the unsafe method "address.call.value()()", but only use certain functions that you can trust won't deplete the gas. ie, depending on the system: develop a function called "safeSend(uint _wei)" which maps to a specific bytecode. This audited signature is first checked (say with getCode) to see if it exists in the bytecode. [Sidenote: address.code is planned to be implemented in Solidity, so this will make it possible].

This is preliminary, and more in depth explorations will need to be done. Potential additional services is to use reputation systems + registries to basically ask a contract: "is this safe?" before executing the code.

Oracles

When using send(), you basically have enough gas to log that it received money. When this happens an oracle can pick up this action and re-issue a transaction, supplying it with gas to essentially "keep going".

Conclusion

A subtlety to remember: if a value transfer happens and the execution fails, the value transfer still happens (due to the stipend). It is not possible to do layered sends() in Solidity. In other words, triggering cascades of sends(). Work-arounds are required. Got any ideas? Let me know. :+1: simon.delarouviere@consensys.net.

Post Script

Also please consider the discussion here: https://www.reddit.com/r/ethereum/comments/47ts9p/is_it_unsafe_to_send_eth_from_contracts_with_the/

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.