A better design for Ether transfer

To transfer Ethers to another contract, developers can use send(), transfer(), and call().

IMHO, this design is problematic.

  1. send() and call() do not throw exceptions, and developers have to manually check their return values to see whether the call is successful. However, it is not enforced by the compiler, so developers can write error-prone code.
  2. transfer() throws exceptions and forces a revert. It is safe, but it seems there is no way for developers to explicitly ignore an exception. The try / catch syntax does not work for transfer().

Explicitness is better than implicitness. Although a simple send() can achieve Ether transfer and ignoring whether it succeeds, I suggest an explicit syntax that enforces safety, and at the same time developers can explicitly ignore an exception. For example:

contract Test {
    function foo(address payable receiver) public {
        try receiver.transfer(1 ether) {
            // do something if it succeeds
        } catch (bytes memory lowLevelData) {
            // can ignore when it fails
        }
    }
}

Or more concisely, a syntax from Swift: (note that in Swift try! has a different meaning)

contract Test {
    function foo(address payable receiver) public {
        try! receiver.transfer(1 ether);
        // will continue to run regardless of whether the transfer succeeds
    }
}

Semi-related, send and transfer do not pass all gas along to the call, despite my repeated pleas for Solidity to stop doing this. :grinning_face_with_smiling_eyes:

1 Like

I think everyone agrees that a proper ether transfer function should have the following properties:

  • forwards all gas by default (can be limited using {gas: ...})
  • reverts by default
  • can be caught using try/catch

The main reason this has no been implemented yet is that

  • changing the behaviour of transfer is a silent breaking change which is very dangerous
  • we could not find a good name for a new function

Unless someone can suggest a good name, maybe a compromise would be to

  • allow .transfer{gas: ...} and maybe also add a shortcut like {gas: max} or {gas: all} and
  • allow try/catch with transfer.
1 Like

It is possible that SELFDESTRUCT opcode will be changing in the (near?) future to effectively be a “send ETH without executing destination contract”. If so, that feels like a perfect time to implement a new transfer function. If that is implemented, would you then be comfortable with a breaking change to .transfer?

It would still be a silent breaking change.

Just created an issue about this in the solidity repo: Allow function call option on `.transfer` and also `{gas: max}` as a special function call option · Issue #11017 · ethereum/solidity · GitHub

It breaks a different invariant though. With SELFDESTRUCT changing to SENDETH it would make it so the receiver doesn’t get called (no opportunity to log). If we changed it to supply all gas the receiver would be able to execute arbitrary code.

In the first case, the risks to dapps are low. Worst case scenario is some contract receives some ETH that it didn’t log (something that can already be done via SELFDESTRUCT anyway). In the second case, the worst case scenario is a reentrancy bug in an app.