2.8 Functions
Functions are the executable units of code. In the case of Solidity
, they are usually defined inside a smart contract, but they can also be defined outside of the contracts in which case they are specified at a file level. Such functions are referred to as "free functions".
Functions are what allow modifications to the state that is encapsulated as part of the contract, so they are how logic manifests itself within the smart contracts and the state transitions from one initial state to the modified state, as a result of any of the transactions or messages that interact with the smart contract.
Parameters
Functions typically specify parameters. These are declared just like variables within the function. Parameters are how the caller of the function sends in data into the function for it to work on.
Parameters are used and assigned in a very similar manner to local variables within the function, and the nomenclature that the function specifies the parameter and the caller sends in arguments that get assigned to these parameters in the context of the function.
Return Variables
Functions typically also return values. These are returned using the return
keyword. Solidity
functions can return single variables or they can return multiple variables. The return variables can also be of 2 types:
Named return variables: they have a specific name or names. They are treated just like local variables within the context of the function.
Unnamed return variables: an explicit return statement needs to be used to return that variable a return value to the context of the function caller.
The caller specifies arguments that get assigned to the respective parameters of the callee function. The caller function works with these parameters (in the context of that function), does something with them along with all the local variables that might be defined within that function (it can also use the state variables that are declared within that contract) and once it is done with that logic, it can return values back to the caller.
Modifiers
Function modifiers are something unique and specific to Solidity
. They are declared using the modifier
keyword and the format is something like this
As you can see, they are very similar to a function where, because modifiers have some logic encapsulated within them. The underscore acts as a placeholder for the function that we're attempting to modify; because modifiers are used along with functions.
So in this case if there is a function foo()
on which this modifier is applied, then whenever this function is called, it goes first to the modifier and depending on any of the checks (any of the logic implemented within that modifier), the function's logic gets called at the point where the underscore is placed within that modifier.
So, if there are a bunch of checks in the modifier prior to the underscore, then those checks implement some preconditions that are evaluated before the function's logic is executed. Similarly, if the underscore precedes the checks in the modifier, the function's logic gets executed first and then the modifier executes its checks.
Examples for the usage here could be access control checks that are implemented as preconditions on the function in the modifier, and they could be post-conditions that could be evaluated if the underscore happens to be before the checks in the modifier, and these could implement some sort of accounting checks in the context of the contract.
Function modifiers play a critical role because they're very often used to implement access control checks, things that allow a contract to specify only certain addresses for example, to call the function where the modifier is applied... This is something that becomes critical when you evaluate the security of smart contracts.
Function Visibility
It is similar to the visibility for state variables functions. Functions have the 4 different visibility specifiers
public
: these functions are part of the contract interface and they can be called either internally (within the contract) or via messages.external
: these functions are also part of the contract interface, which means they can be called from other contracts and via transactions, but they cannot be called internally.internal
: these functions on the other hand can only be accessed internally (from within the current contract or contracts deriving from it).private
: these functions can be accessed only from within the contract where they are defined and not even from the derived contracts.
Function Mutability
Similar to the state variable mutability, functions also have the concept of mutability. This affects what state can they read or modify. Depending on that there are two function mutability specifiers:
view
: these functions are allowed only to read the state but not modifying it. This is enforced at the EVM level using theSTATICCALL
opcode.\There are various actions that are considered as state modifying that are not allowed for view functions, these include:
Writing to state variables (as should be obvious)
Emitting events
Creating other contracts
Using self-destruct
Sending ether to other contracts
Calling other functions not marked
view
orpure
Using low level calls
Using inline assembly that contain certain opcodes
pure
: these on the other hand are allowed to neither read contract state nor modify it.\The not modification part can be enforced at the EVM level, but the reading part cannot because there are no specific opcodes that allow that. There are various actions that are considered as reading from the state:
Reading from state variables (obviously)
Accessing the balance of contracts
Accessing members of
block
Transactions or messages
Calling other functions not marked as
pure
Using inline assembly that contain certain opcodes
The read/write mutability aspect of functions again has security implications as you can imagine.
Function Overloading
This is something fundamental to object oriented programming. It means that it supports multiple functions within a contract to have the same name but with different parameters or different parameter types. Overloaded functions are selected by matching the function declarations within the current scope to the arguments supplied in the function call, so depending on the number and the type of arguments the correct function is correctly chosen.
Note that return variables are not considered for the process of resolving overloading, so this notion of overloading is an interesting one that is supported by Solidity
given that it is an object-oriented programming language.
Free Functions
They are functions that are defined at the file level (i.e. outside the scope of contracts) and thus these are different from the contract functions (defined within the scope of the contract). Free functions always have implicit internal
visibility and their code is included in all the contracts that call them, similar to internal library functions. These functions are not very commonly seen.
Special Functions
Constructor
This concept is specific and unique to Solidity
because it applies to smart contracts and the way they are created on Ethereum. Recall contracts on ethereum can be created from outside the blockchain via transactions, or from within the Solidity
contracts themselves. When a contract is created, you can imagine that one would want to initialize the contract state in some manner. This is made possible by the constructor.
So the constructor is a special function that gets triggered when a contract is created. A constructor is optional and there can be only one constructor for every contract. These special functions are specified by using the constructor
keyword; some of the syntax and semantics have changed over the course of time, but this is how it has been in the most recent versions of Solidity
So constructors are used to initialize the state of a contract when they are created and deployed on the blockchain. They're triggered when a contract is created and it's run only once. Once the constructor has finished executing, the final code of the contract is stored on the blockchain and this deployed code does not include the constructor code or any of the internal functions that are called from within the constructor.
From a security perspective, constructors are very interesting because they allow one to examine what initializations are being done to the contact state because if not, the default values of the specific types of state variables are used instead. For example it could be used in the context of the various contract functions, which is an interesting and important aspect when it comes to evaluating the security of smart contracts.
Receive Function
Another special function in the context of Solidity
is the receive()
function. This function gets triggered automatically whenever there is an Ether transfer made to this contract via send()
or transfer()
primitives.
It also gets triggered when a transaction targets the contract but with empty CALLDATA
. Recall that a transaction that targets a contract specifies which function needs to be called in that contract and what arguments need to be used within the data portion of the transaction, but if that data is empty then the receive function is the function that gets automatically triggered in the contract.
There can only be one receive()
for every contract and this function cannot have any arguments, it cannot return anything and it must also have external visibility and a payable
state mutability.
payable
state mutability is something we haven't discussed so far but what it specifies is that the function that has this payable
specifier can receive Ether as part of a transaction and that applies to the receive function as well because it is triggered when Ether transfers happen.
The send and transfer primitives are designed in Solidity
to transfer only 2300 gas. The rationale behind this was to prevent the risk, or mitigate the risk of what are known as "reentrancy attacks" which we'll talk more in the security chapter. This minimal amount of gas does not allow a receive()
function to do anything much more than some basic logging (using events).
From a security context, the receive()
function becomes interesting to evaluate because it affects the Ether balance of a contract and any assumptions in the contract logic that depends on the contract's Ether balance.
Fallback Function
Yet another special function in Solidity
. It is very similar to the receive()
function with some particularities: the fallback()
function gets triggered automatically on a call to the contract if none of the functions in the contract match the function signature specified in the transaction. It also gets triggered if there was no data supplied at all in the transaction and there is no receive()
function.
Similar to receive()
, there can be only one fallback()
for every contract, however this fallback()
function can receive and return data if required. The visibility is external
and if the fallback()
function is meant to receive Ether, then it needs to have the payable
modifier specified (similar to the receive()
function).
The fallback()
function cannot assume that more than 2300 gas can be supplied to it because it can be triggered via the send
or transfer
primitives and, similar to receive()
, the security implications of fallback()
have to consider that the Ether balance can be changed via this function, so any assumptions in the contract logic specific to the Ether balance need to be examined.
Last updated