In this article, we will focus on the second topic of the interesting pair, the fallback function, as a follow-up on the receive ether function discussed in the previous article.
It’s part of our long-standing tradition to make this (and other) articles a faithful companion, or a supplement to the official Solidity documentation.
Overview
The purpose behind a fallback function, as the name suggests, is to serve as a function that executes when:
- A contract receives a call but cannot match any other function by the function signature;
- When no data is supplied with the contract call and the contract doesn’t have the receive Ether function.
In a way, we can consider a fallback function as a default function that gets executed in the situations listed above.
Implementation
A fallback function must be declared with external visibility. One more important and potentially needed option is declaring a fallback function as virtual, meaning it can be overridden in an inheriting contract.
Declaring a fallback function as payable
is possible, but not necessary; we’d declare it as payable
only if the function must be able to receive Ether.
π€ Recommended: What is βpayableβ in Solidity?
However, it’s a better practice to always define a receive()
function, even if we do have a fallback()
function. In such case, a fallback function would be used only for otherwise unmatched function calls. A fallback function can also use function modifiers.
A smart contract can have at most one fallback function declared in one of the two following forms, both omitting the keyword function
which we’d usually use when defining an ordinary function:
fallback() external [payable]
or
fallback(bytes calldata input) external [payable] returns (bytes memory output)
If we use the second form of a fallback function declaration instead of a receive Ether function, input
will contain the full data sent to the contract (equal to msg.data
) and it will return msg.data
in output (check the console log after calling the function). The returned data will not be ABI-encoded and will be returned in the original form (without padding).
π‘ Note: If a fallback function is activated when a contract is called from a .send()
or .transfer()
function, it will be limited to at most 2300 gas, which allows only basic logging. The same goes for the receive()
function in the previous article.
Operations consuming more than 2300 gas are:
- writing to storage,
- creating a contract,
- calling an external function that consumes a large amount of gas, and
- sending Ether.
They can only execute if the fallback function is called by the low-level .call
function.
In general, a fallback function can do anything, including some complex operations. The only condition is that enough gas is available to execute the operations.
π‘ Recommended: Introduction to Ethereumβs Gas in Solidity Development
β‘ Warning: A payable fallback
function also enables our contract to receive Ether when a receive Ether function is missing. Regardless, it is recommended to implement a receive Ether
function as it will specifically handle the Ether transfers. The fallback function will only be used for “interface confusion” cases, i.e., when a contract receives a function call to a non-existing function.
To decode the input data of a function, we can check the first four bytes which represent the function selector and are compared to available function signatures. Then, we can use abi.decode(...)
function combined with the array slice syntax for decoding the ABI-encoded (Abstract Binary Interface) data:
(c, d) = abi.decode(input[4:], (uint256, uint256));
However, we shouldn’t be comfortable with this approach and use it only when no other is available. Instead, we should rather use available functions.
Example – The Source Code
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; contract Test { uint x; fallback() external { x = 1; } } contract TestPayable { uint x; uint y; fallback() external payable { x = 1; y = msg.value; } receive() external payable { x = 2; y = msg.value; } } contract Caller { function callTest(Test test) public returns (bool) { (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success); address payable testPayable = payable(address(test)); return testPayable.send(2 ether); } function callTestPayable(TestPayable test) public returns (bool) { (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success); (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()")); require(success); (success,) = address(test).call{value: 2 ether}(""); require(success); return true; } receive() external payable {} }
Example – The Fallback Function
Let’s look at the contract example, showing what a fallback function looks like.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.2 <0.9.0; contract Test { uint x; fallback() external { x = 1; } }
The fallback function will execute every time since no other function is declared, i.e., no function signature would match a function call.
Also, if we try sending Ether to this contract, an exception will be thrown since the fallback function is not defined as payable
and it cannot receive Ether.
This can be fixed in two steps:
- Change the
external
keyword toexternal payable
; - Remove
x = 1;
because this operation takes more than 2300 gas available.
We won’t make the change here, but in case you want to play with it, that’s how you can do it.
Example – Fallback Function + receive Ether Function
This time, let’s look at a contract example showing a fallback function and a receive Ether function, according to the recommendation on a good way to structure our smart contract projects.
contract TestPayable { uint x; uint y;
The fallback function is called for all unmatched function messages sent to this contract, except for plain Ether transfers (only when there’s a receive function).
fallback() external payable { x = 1; y = msg.value; }
Plain Ether transfers are calls with empty calldata
and are processed by the receive
function.
receive() external payable { x = 2; y = msg.value; } }
In this example, we’ll build on both of the two previous examples and show how calls to the two examples differ.
contract Caller { function callTest(Test test) public returns (bool) {
The test
parameter receives an argument, i.e., a destination contract address.
Then, a call to the non-existing function is deliberately made. A tuple is returned: the first part of the result is stored in the success variable, while the rest is discarded.
The fallback()
function will execute, and the function call will complete with the success variable set to true.
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success);
Before we can call the send()
function on the contract address received via parameter test
, we must convert it from the address
type to the address payable
type.
This will allow us to call the send()
function from the caller’s side, but the call itself will fail on the callee’s side because the fallback()
function isn’t declared as payable
, i.e., it cannot receive Ether.
address payable testPayable = payable(address(test)); return testPayable.send(2 ether); }
In contrast to the previous callTest()
function, the callTestPayable()
function can successfully send Ether to the destination contract because the answering functions are both declared as payable
.
function callTestPayable(TestPayable test) public returns (bool) {
The test
parameter receives an argument, i.e., a destination contract address. Then, a call to the non-existing function is deliberately made.
We use different hard-coded values in all three calls to distinguish when a receive()
function is executed (hard-coded value test.x = 2
) instead of the fallback()
function (hard-coded value test.x = 1
).
Our receive()
and fallback()
functions both receive Ether and write to storage (x
and y
variables). As writing to storage requires more than 2300 gas, we wouldn’t be able to do both with send()
function but instead had to use the low-level call()
function.
The next function call is also with calldata, so a fallback()
function is executed; test.x
becomes = 1
(hard coded), and test.y
becomes = 0
(a default value, since no value is sent).
A tuple is returned: the first part of the result is stored in the success variable, while the rest is discarded. The fallback()
function will execute and the function call will complete with the success variable set to true.
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success);
The next function call is with calldata
, but also with a value, so a fallback function is executed; test.x
becomes = 1
(hardcoded assignment in the fallback function) and test.y
becomes = 1
(parametrized assignment).
(success,) = address(test).call{value: 1} (abi.encodeWithSignature("nonExistingFunction()")); require(success);
The next call is without calldata
, so the receive function is executed; test.x
becomes = 2
(hard-coded assignment in the receive function) and test.y
becomes 2000000000000000000
(a parametrized assignment in the amount of Wei units equal to 2 Ether sent as a value).
(success,) = address(test).call{value: 2 ether}(""); require(success); // results in test.x becoming == 2 and test.y becoming 2 ether. return true; } }
Conclusion
In this article, we learned about a fallback function, i.e., what it is, how, and when it’s used.
First, we made an overview of the fallback function.
Second, we learned about how a fallback function is implemented and what its limitations are.
Third, we listed the source code for easier navigation.
Fourth, we viewed a simple fallback function example.
Fifth, we examined a more thorough, complex example combining both a fallback and a receive function.
What’s Next?
This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):