Implementation of `receive` unexpectedly impacts `fallback`

Problem Statement

When developer implements receive function while fallback is already implemented, part of requests early handled by fallback are redirected to receive function.

I see this behavior as non-obvious. Developer may add a dependency which implements receive function or whatever and this might affect initial solution in a hidden way.

Details

Here is the table of which function processes request depending on receive/fallback existence and modifiers. It can be seen that receive and fallback functionalities are not complement and receive overrides fallback in case of combination.

Value Data Receive Fallback Fallback Payable Receive + Fallback Receive + Fallback Payable
no no receive fallback fallback receive receive
no some revert fallback fallback fallback fallback
some no receive revert fallback receive receive
some some revert revert fallback revert fallback

Additionally, there is a compiler warnings that propose to implement receive function if fallback payable is implemented. This is quite confusing and may lead to initial functionality break in case of receive doesn’t just copy fallback.

Proposal

From my point of view, receive/fallback should complement each other, example provided below. This lowers flexibility but strictly distinguishes responsibility area for each of the functions.

Value Data Receive Fallback Fallback Payable Receive + Fallback Receive + Fallback Payable
no no receive revert revert receive receive
no some revert fallback fallback fallback fallback
some no receive revert revert receive receive
some some revert revert fallback revert fallback

Backward Compatibility

This breaks backward compatibility, so can’t be implemented until 0.9.0.

Related Discussions

2 Likes

Example

In the code below, the receive function of the Dependency contract implicitly impacts the behavior of the fallback function of the Target contract.

The fallback function in Target does not handle calls with empty msg.data because those calls are intercepted by the receive function in Dependency. However, if the receive function would not exist in Dependency, the fallback function will also handle calls with empty msg.data.

It should be noted, that the receive function in Dependency is just reverting and does not look bad in the context of that particular contract, in a such way, it is a high likelihood that the function is not removed as it looks harmless. Additionally, in a real-world scenario, this receive function might be hidden deep within a dependency tree, making it harder to spot it.

contract Dependency {
    error TransfersNotAllowed();

    receive() external payable {
        revert TransfersNotAllowed();
    }
}

contract Target is Dependency {
    function _fallbackLogic() internal {}

    fallback() external payable {
        _fallbackLogic();
    }
}
1 Like

Thanks for taking the time to write about this topic and encouraging visibility around it. It’s quite an interesting problem and I’m curious to see if there are others in the community who have opinions on the proposal. :slight_smile: