In our last article we were talking about functions in Solidity. However, we did not cover everything on that subject. In Solidity we have some additional features that could be called special functions.

We have the following special functions in Solidity:
  • Getter functions
  • Constructor
  • Receive function
  • Fallback function
  • Selfdestruct – rather a keyword than a function

Ok, let’s take a closer look at each them…

Getter functions:

We already talked about them in our last article. Whenever you declare a public state variable, the compiler will automatically create a public getter function, which allows us to retrieve the value of that state variable.

However, if your public state variable is an array, the corresponding getter function allows you to retrieve only one array element at a time, by specifying a valid index of that array.

If you want to call a Getter function from another contract, you simply specify the name of the corresponding state variable with brackets together with the name of the contract.

For example: myContract.somePublicStateVariable();

Constructors:

The constructor is called only once during the deployment of the contract and it allow us to initialize the state variables of the contract. We can also provide arguments and if we want to transfer funds to the contract during deployment, we need to add the “payable” keyword to the declaration of the constructor.

Receive:

Each contract can have one receive function. The function cannot have any arguments, it cannot return anything, it must have external visibility and be payable. We don’t use the “function” keyword in the declaration of the receive function:

receive() external payable { … }

The receive function is executed on plain Ether transfers (via .send() or .transfer() ). It is important to note that the receive function can rely only on 2300 units of gas. This means, you cannot execute much code in the function. Typically, you will emit an event in the receive function, but you cannot update any state variables, because this would require more gas.

Fallback:

Again, each contract can have one fallback function. The function needs to be external, it can accept one argument of type “bytes calldata” and it can return one argument of type “bytes memory“.

The fallback function is executed on a call to the contract when none of the other contract functions match the provided function signature or if  no data was supplied with the function call and the contract does not have a receive function.

If the fallback function needs to be able to receive Ether, it needs to be marked payable. If a contract neither has  receive function nor a payable fallback function, it cannot receive Ether through a regular transfer.

fallback ([bytes calldata input]) external [payable] [returns (bytes memory output)]

Selfdestruct:

This is not a function, but a Solidity keyword you can use to render your smart contract unusable. Calling the “self-destruct” keyword will delete the smart contract bytecode from the blockchain. Of course, the entire past transaction history will remain on the blockchain due to the immutable nature of the Ethereum blockchain.

Together with the self-destruct keyword, you also need to specify an address (typically the owner of the contract) to which any remaining funds in the contract will be sent.

If someone sends funds to a contract address after self-destruct has been called, those funds will be lost forever.

Now, let’s write some code to test the various special functions in Solidity.

As usual, you can copy/paste the code below into Remix and play around with it:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract SpecialFunctions {
    uint public value;
    address payable public owner;

    event ReceivedEther(address sender, uint amount, uint balance);

    constructor (uint _initialValue) {
        value = _initialValue;
        owner = payable(msg.sender);
    }

    function getBalance() public view returns(uint) {
        return address(this).balance;        
    }

    receive() external payable {
        emit ReceivedEther(msg.sender, msg.value, address(this).balance);
    }

    fallback() external {
        value = 10;        
    }

    function destroySmartContract() public {
        if (msg.sender == owner) {
            selfdestruct(owner);
        }
    }
}

contract TestSpecialFunctions {

    SpecialFunctions public sf;

    constructor(SpecialFunctions _sf) payable {
        sf = _sf;
    }

    function triggerFallback() public {
        (bool success,) = address(sf).call(abi.encodeWithSignature("test2()"));
        require(success, "The call failed!");
    }

    function triggerReceive() public  {
        payable(address(sf)).transfer(50);
    }

    function getValue() public view returns(uint) {
        return sf.value();
    }
}

The example is very simple and pretty much self-explanatory. We declare 2 public state variables, which are initialized in the constructor. Then, we define a receive function that emits an Event and a fallback function that will be called when none of the other contract functions match the provided function signature. And, finally, we destroy the contract by calling the “selfdestruct” keyword.

In the second contract: “TestSpecialFunctions“, we are triggering the receive and fallback functions. We create a state variable “sf” with the type of our first contract (“SpecialFunctions”) and we assign the corresponding address in the constructor.

In the “triggerFallback” function, we issue a low level call to a non-existent function: “test2”. We can’t simply call: sf.test2(); , because test2 does not exist on the “SpecialFunctions” contract and the compiler would generate an error.

However, we can use the low-level “call” method on the address type and specify the encoded calldata. To get our encoded calldata, we simply use the abi.encodeWithSignature method and we specify the signature of a non-existent function.

Of course, test2() does not exist on the “SpecialFunctions” contract and the fallback function will be called, which sets the value state variable to 10.

When we call “triggerReceive“, we transfer 50 wei to the “SpecialFunctions” contract. To do so, we need to cast the “SpecialFunctions” type (sf) to a payable address and then we can call the “transfer” method with the amount of wei we want to transfer to the “SpecialFunctions” contract.

We are issuing a plain Ether transfer (well, wei in our case), which will trigger the receive function on the “SpecialFunctions” contract. In the receive function, we only have 2300 gas available, that means we can’t execute much code, but it’s enough to emit an event – “ReceivedEther” in our example.

It’s time to test that second contract on Remix:
  • Deploy the first contract: “SpecialFunctions” and specify a value for the “_initialValue” argument.
  • Deploy the second contract: “TestSpecialFunctions“, specify the address of the “SpecialFunctions” contract you just deployed for the “_sf” argument and send some Ether along (for example, 100 wei).
  • On “TestSpecialFunctions”, call the “getValue” function. It should return the value you provided when you deployed the first contract.
  • Call the “triggerFallback” function. And, then, call the “getValue” function again. It should now return 10, the value we hardcoded in the fallback function, which proves, that the fallback function was triggered on calling “triggerFallback”.
  • On the first contract: “SpecialFunctions”, call the “getBalance” function. It should return 0, because we did not transfer any Ether to that contract yet.
  • On the second contract, call the “triggerReceive” function. Now, call the “getBalance” function again. It should return 50, the amount we are sending to the “SpecialFunctions” contract in the “triggerReceive” function.

In Remix, open the transaction at the bottom of the window and scroll down to the logs section. Here you can see the details of the event that was emitted in the receive function: