[Documentation] Conversion from payable to contract type

Solidity docs states “Explicit conversion to and from the address payable type is only possible if the contract type has a receive or payable fallback function.”

However, I am able to explicitly convert address payable to contract type that does not have ability to receive either.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Car {}

contract TestContract {
    Car c;

    function test() public {
        c = Car(payable(address(1)));
    }
}

Shouldn’t this throw a compile time error?

It cannot throw a compile-time error, because checking whether or not a contract “has a receive or payable fallback function” can only take place during runtime.

Same goes for checking whether or not an address is that of a contract (i.e., checking whether or not a contract is deployed at a given address).

Moreover, an arbirtary address can “become an address of a contract” at any point in the future, and that contract may or may not implement those fuctions (which again, points to the fact that this check is not something that can or should take place during compile-time).

So if your claim about this being stated in the Solidity documentation is correct, then the documentation probably needs to be fixed.

I nevertheless sincerely doubt that the official documentation actually states something like this; please provide a link to that…

Hi Barakman, thanks for the detailed response. What you describe makes perfect sense. Here is the link to the documentation: Types — Solidity 0.8.21 documentation.

The quoted excerpt is the first sentence of the second paragraph under the heading “Contract Types”.

I’ve extracted the following bullets out of the official documentation:

  1. Explicit conversion to and from the address payable type is only possible if the contract type has a receive or payable fallback function.
  2. If the contract type does not have a receive or payable fallback function, the conversion to address payable can be done using payable(address(x)).

This is indeed somewhat confusing, so let’s clarify it with a coding example:

// does not implement a receive or payable fallback function
contract N {
}

// does implement a receive or payable fallback function
contract P {
    receive() external payable {}
}

contract Test {
    function func1(N x) public pure {address y = address(x);}
    function func2(P x) public pure {address y = address(x);}
    function func3(N x) public pure {address payable y = payable(address(x));}
    function func4(P x) public pure {address payable y = payable(address(x));}
    function func5(address x) public pure {N y = N(x);}
    function func6(address x) public pure {P y = P(x);}
    function func7(address payable x) public pure {N y = N(x);}
    function func8(address payable x) public pure {P y = P(x);}
}

Functions 1 and 2 will compile, because we can cast any contract type to an address type.

Functions 3 and 4 will compile, because we can cast any contract type to an address payable type.

Functions 7 and 8 will compile, because we can cast an address payable type to any contract type.

Function 5 will compile, because we can cast an address type to any “non payable” contract type.

Function 6 will not compile, because we cannot cast an address type to a “payable” contract type.

In this case, we must explicitly cast the address type to an address payable type:

function func6(address x) public pure {P y = P(payable(x));}

Note that the compiler does not protect you from any of the following potential runtime errors:

  • In function 3, if you attempt to transfer ETH to y, which points to a contract which does not implement a receive or payable fallback function.

  • In functions 5 thru 8, if x does not point to a contract of the specified type, and you attempt to call any of the functions of that contract type via y.

And finally, referring to the coding example in your question:

There are two castings here:

The first one is the casting of the constant integer 1 to an address payable type.
This is of course outside the context of this thread, which deals with casting between contract types and address types, but just for the protocol - there is obviously no problem in doing so (at your own risk of course).

The second one is the casting of an address payable type to Car - a contract type which does not implement a “payable infrastructure” (a receive or payable fallback function).

But what exactly makes you think that there should be a problem here in the first place?

This is also evident in your actual question:

However, I am able to explicitly convert address payable to contract type that does not have ability to receive either.

So what? You’re only taking an address which can receive ether, and preventing it from receiving ether; There is no compile-time problem here, and moreover - no runtime problem either (not that it would make any difference if there was, since we’re dealing with the compiler here, but still).

So in short, I think that you got a little confused in the question itself, phrasing it in a manner which is not even related to what you’ve pointed out regarding the official documentation.

Hi @barakman, thanks again for such a detailed perspective on this. Your sentence perfectly captures the essence of what is happening:

So what? You’re only taking an address which can receive ether, and preventing it from receiving ether;

I inferred the following

Explicit conversion to and from the address payable type is only possible if the contract type has a receive or payable fallback function.

as

Explicit conversion FROM the address payable type is NOT POSSIBLE if the contract type DOES NOT HAVE a receive or payable fallback function.

I was hoping that the compiler would somehow prevent this from happening.

What you have made very clear is that it CANNOT be prevented by the compiler for perfectly valid reasons. And this conversion is NOT a big deal to begin with because I’m just restricting the ability to send ether to a contract.

As I explained above, there is absolutely no need whatsoever to hope for something like this.

For example, you take some address payable x and cast it to:

NonPayableContract y = NonPayableContract(x);

Then you use y for whatever… you are not likely to send ETH to y because you’ve explicitly implemented this contract as “non-payable”, right? So who cares that the original address was declared payable?

If there was anything to hope for, it’s the exact opposite scenario:

Explicit conversion TO the address payable type is NOT POSSIBLE if the contract type DOES NOT HAVE a receive or payable fallback function.

For example, you take some NonPayableContract x and cast it to:

address payable y = payable(address(x));

Then you use y for whatever… you are very likely to send ETH to y because you’ve explicitly declared this address as “payable”, right? But doing so would revert, since y is in fact the address of a contract with no receive or payable fallback function.

Unfortunately, the compiler cannot detect this hazardous scenario.

2 Likes

Exactly! Thanks again for your time, really appreciate it!

1 Like