Exploring the Differences Between Payable and Non-Payable Functions in Solidity: An In-Depth Analysis

Oluwatosin Serah
Coinmonks
Published in
7 min readMar 27, 2023

--

Welcome to another article on Solidity’s payable and non-payable functions! If you’re interested in blockchain development and smart contracts, you’ve probably come across these terms before. In this article, we’ll explain what payable and non-payable functions are, how they work in Solidity, and why payable functions are less costly (in terms of gas) than non-payable ones. We’ll also explore some use cases for both types of functions and highlight the trade-offs that developers need to consider when choosing between them.
By the end of this article, you should have a clear understanding of how to use payable and non-payable functions effectively in your Solidity smart contracts. Let’s dive in!

Payable Functions

In Solidity, a payable function is a function that can accept ether as an input. When a contract is called with a payable function, the caller can send ether along with the function call. The ether is then stored in the contract’s balance, which can be accessed using the “address(this).balance” property.

How to declare a payable function

In Solidity, you use the payable function when you want to allow a contract to receive ether. A payable function is declared using the ‘payable’ keyword, which makes it possible to receive ether as part of a function call. See code example below;

The code above defines a contract that allows users to deposit ether into their accounts by calling the deposit function.

Non Payable Function

A non-payable function, on the other hand, is a function that cannot accept ether as an input. If a contract is called with a non-payable function and the caller tries to send ether with the call, it will result in an error. See code example below;

Using msg.value in non-payable functions

In Solidity, using "msg.value" in non-payable functions is not permitted for security reasons. "msg.value" represents the amount of ether being sent with the function call. If it is used in a non-payable function, this indicates that the function is receiving ether, which was not intended. This could result in security problems and potential financial loss.

To handle this scenario, you need to either make the function payable or create a new internal function that uses “msg.value”. If the function needs to receive ether, you should make it payable by adding the payable keyword before the function definition. This allows the function to receive and process ether.

Functions that are implicitly payable

1. Receive function: The receive function is automatically payable and is called when the call data is empty. This means that if someone sends a transaction to the contract without specifying which function to call, the receive function will be executed. It is declared like this;

receive() external payable{}

2. Fallback function: The fallback function, on the other hand, is triggered when you call a function that doesn’t exist or match any function in a contract. If the receive function does not exist, the fallback function will handle calls with empty call data. If a fallback function is not payable, transactions or function calls that do not match any other function and also send value (ether) will revert. It is declared like this;

fallback() external payable{}

Explicit address conversion

It is important to note that not all addresses are payable, and if you try to send ethers to a non-payable address, the transaction will fail.

Explicit conversion from address to address payable is only possible if the contract has a receive or payable fallback function. The conversion can be performed using address(x), where x must be of type address.

However, 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 because a contract without a receive or payable fallback function cannot receive ether and therefore cannot be converted to a payable address.

In essence, the conversion from address to address payable is done to enable sending ether to the address. This conversion is used when calling a contract function that has the “payable” modifier, which allows it to receive ether. The “payable” keyword makes the function accept ether and increases the balance of the contract by the amount of ether received. See code example below;

//address payable provides the transfer function
address payable payableAddress;

Gas cost analysis for payable and non payable function

Let’s perform a transaction and observe the gas cost for each case using the following contracts.

The depositEther transaction in the payable contract costs 30605 gas while the depositAmount transaction in the non payable contract costs 31141 gas. Notice that the gas cost for the payable function is cheaper than that of non payable function. Let’s unbox the mystery behind it!📜

Why payable functions are cheaper than non payable functions

Using this code below, we will shed light on why payable functions are more cost-effective than non-payable functions.

Instead of using the previous code, which the bytecode and opcode of the contracts would have been overly complex and lengthy, I will be utilizing the empty constructor function. By doing so, it will be easier for everyone to comprehend and subsequently apply the knowledge to other contracts without any confusion.

Init Code

The init code, also known as the constructor code, is executed only once when the contract is deployed. Its purpose is to initialize the contract’s state and set its initial values. The init code is used to create and store the contract’s variables, set up the contract’s logic, and perform any other setup tasks required for the contract to operate correctly.

Runtime Code

Runtime code is executed every time the contract is called or invoked. This code defines the logic of the contract, including how it interacts with other contracts, how it stores and retrieves data, and how it handles transactions. The runtime code is what executes when a contract is interacted with.

Bytecode for the Non-Payable contract

[6080604052"348015600f57600080fd5b50603f80601d"6000396000f3fe] - Init Code
[6080604052600080fdfea2646970667358fe1220fefefefefe160342fefe35692fec668c4c7705aa8c111a45fe6447aec315841164736f6c63430008110033]- Runtime code

Bytecode for the Payable contract

[6080604052"603f806011600039"6000f3fe] - Init Code
[6080604052600080fdfea2646970667358fe1220fe09fefe93fef3f2a4fefefea49cfe11fefe99fe05fe7d05fa3e9cd05f3e143364736f6c63430008110033]- Runtime code

If you observe, the init code for the non-payable function is longer than that of the payable function same goes for the opcodes below. The init is highlighted in double quotes.

Opcode for the Non- payable contract

//Set up the free memory pointer
[00] PUSH1 80
[02] PUSH1 40
[04] MSTORE //Stores the word in memory
[05] CALLVALUE -> Is the amount in wei that was with a transaction
[06] DUP1
[07] ISZERO
[08] PUSH1 0f
[0a] JUMPI
[0b] PUSH1 00
[0d] DUP1
[0e] REVERT
[0f] JUMPDEST
[10] POP
[11] PUSH1 3f
[13] DUP1
[14] PUSH1 1d ->
[16] PUSH1 00
[18] CODECOPY
[19] PUSH1 00
[1b] RETURN
[1c] INVALID
[1d] PUSH1 80
[1f] PUSH1 40
[21] MSTORE
[22] PUSH1 00
[24] DUP1
[25] REVERT
[26] INVALID
[27] LOG2
[28] PUSH5 6970667358
[2e] INVALID
[2f] SLT
[30] SHA3
[31] PUSH31 bc71976c3da4aae3dc9fb5312d39cd8267899b40f5cbb457a9ed9feeec97d9
[51] PUSH5 736f6c6343
[57] STOP
[58] ADDMOD
[59] GT
[5a] STOP
[5b] CALLER

The commented opcodes perform the following actions: the ‘CALLVALUE’ checks the amount in wei sent with the transaction, duplicates it with the ‘DUP’ opcode, and then determines if it’s equal to zero using the ‘ISZERO’ opcode.

If the value is zero, the ‘ISZERO’ opcode pushes 1 onto the stack, which means ‘true’, and continues execution. If not, it pushes 0, meaning ‘false’.

In this case, the ‘CALLVALUE’ is expected to be zero since the constructor wasn’t set to be payable, meaning no value was intended to be sent with the transaction. If a value is mistakenly sent, the execution will eventually revert, as indicated by the ‘INVALID’ opcode.

Opcode for the Payable contract

//Set up the free memory pointer
[00] PUSH1 80
[02] PUSH1 40
[04] MSTORE
[05] PUSH1 3f ->
[07] DUP1
[08] PUSH1 11
[0a] PUSH1 00
[0c] CODECOPY ->
[0d] PUSH1 00
[0f] RETURN
[10] INVALID
[11] PUSH1 80
[13] PUSH1 40
[15] MSTORE
[16] PUSH1 00
[18] DUP1
[19] REVERT
[1a] INVALID
[1b] LOG2
[1c] PUSH5 6970667358
[22] INVALID
[23] SLT
[24] SHA3
[25] INVALID
[26] MULMOD
[27] INVALID
[28] INVALID
[29] SWAP4
[2a] INVALID
[2b] RETURN
[2c] CALLCODE
[2d] LOG4
[2e] INVALID
[2f] INVALID
[30] INVALID
[31] LOG4
[32] SWAP13
[33] INVALID
[34] GT
[35] INVALID
[36] INVALID
[37] SWAP10
[38] INVALID
[39] SDIV
[3a] INVALID
[3b] PUSH30 05fa3e9cd05f3e143364736f6c63430008110033

For the payable contract, the EVM skips the checks as it assumes that a value will be sent with the transaction resulting in a lower gas cost compared to the non payable contract.

Conclusion

To summarize the gas cost analysis, the use of the ‘CALLVALUE’ and other opcodes which follow for non-payable functions results in increased gas costs, as each opcode incurs a gas fee.

Thank you for joining me on this journey through Solidity’s payable and non-payable functions! I trust that you now have a better understanding of how these functions operate, the distinctions between them, and why payable functions are a more gas-efficient alternative in Solidity smart contracts. If this article has been helpful to you, ensure you like, comment, follow me, and share this article with others so that it can reach a wider audience.

--

--

Oluwatosin Serah
Coinmonks

Smart Contract Developer || Blockchain Educator || Technical Writer || Researcher