
In Solidity, you can pass functions as arguments to other functions or store them in variables. This technique is often referred to as function injection or function pointers. It allows for greater flexibility, as you can dynamically choose which function to call based on certain conditions.
For beginners and intermediate developers, this can seem confusing at first because the syntax is a bit different from how we usually call functions. In this article, we'll break it down step-by-step with clear examples to help you understand how function injection works in Solidity.
Function injection in Solidity refers to the concept of passing a function as a parameter or storing a function in a variable so that it can be called dynamically later. This gives your code more flexibility, as you can choose which function to call based on different conditions.
In Solidity, you can declare a function type as a variable, and later assign it a specific function that matches the same signature (input parameters and return types). Once assigned, you can invoke that function just like any other function call.
Let's start with a simple example to understand how it works.
Imagine you are building a basic calculator contract, and you want to dynamically choose between different arithmetic operations like addition, subtraction, multiplication, and division based on user input.
Here’s how you could do that using function injection.
We’ll start by defining internal functions that perform the basic arithmetic operations (add, subtract, multiply, divide).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Calculator {
// Basic arithmetic functions
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function subtract(uint a, uint b) internal pure returns (uint) {
return a - b;
}
function multiply(uint a, uint b) internal pure returns (uint) {
return a * b;
}
function divide(uint a, uint b) internal pure returns (uint) {
require(b > 0, "Cannot divide by zero");
return a / b;
}
}
Each function takes two numbers (a and b) and returns the result of the respective operation.
Next, we'll create a function type variable to store a reference to any of the above functions. This variable will allow us to dynamically choose which operation to perform.
function (uint, uint) internal pure returns (uint) operation;
This line of code declares a variable operation that can hold any function matching the following signature:
Takes two uint parameters.
Returns a uint.
Is an internal and pure function.
Now, let's create a function that accepts an operation type (addition, subtraction, etc.) and uses function injection to call the correct function dynamically.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Calculator {
// Define basic arithmetic functions
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function subtract(uint a, uint b) internal pure returns (uint) {
return a - b;
}
function multiply(uint a, uint b) internal pure returns (uint) {
return a * b;
}
function divide(uint a, uint b) internal pure returns (uint) {
require(b > 0, "Cannot divide by zero");
return a / b;
}
// Use function injection to perform the desired operation
function calculate(uint a, uint b, string memory operationType) public pure returns (uint result) {
function (uint, uint) internal pure returns (uint) operation;
// Dynamically assign the correct operation based on input
if (keccak256(bytes(operationType)) == keccak256(bytes("add"))) {
operation = add;
} else if (keccak256(bytes(operationType)) == keccak256(bytes("subtract"))) {
operation = subtract;
} else if (keccak256(bytes(operationType)) == keccak256(bytes("multiply"))) {
operation = multiply;
} else if (keccak256(bytes(operationType)) == keccak256(bytes("divide"))) {
operation = divide;
} else {
revert("Unknown operation type");
}
// Call the injected function and return the result
return operation(a, b);
}
}
operation: This is the function type variable that can hold any internal function with the signature (uint, uint) -> uint.
String Matching: Based on the operationType string passed to the calculate function, the correct function (add, subtract, multiply, or divide) is assigned to operation.
Function Call: Once the function is assigned to operation, we can call it using operation(a, b), and it will return the result of the appropriate arithmetic operation.
Calling calculate to Add:
calculate(10, 5, "add") // returns 15
Calling calculate to Multiply:
calculate(10, 5, "multiply") // returns 50
Calling calculate to Divide:
calculate(10, 2, "divide") // returns 5
In Solidity, you can create variables that can store references to functions. These variables allow you to dynamically decide which function to call at runtime. In our example, operation is a variable that can hold any function that:
Is internal (i.e., can only be called inside the contract).
Is pure (i.e., it doesn’t read or modify the contract’s state).
Takes two uint parameters and returns a uint.
if (keccak256(bytes(operationType)) == keccak256(bytes("add"))) {
operation = add;
}
Here, we're checking the string passed in ("add", "subtract", etc.), and based on the result, we assign the corresponding function (add, subtract, multiply, or divide) to the operation variable.
Once we assign a function to the variable operation, we can call it like a normal function:
return operation(a, b);
This dynamically calls the correct function based on the input. If operation points to add, it will perform the addition. If it points to divide, it will perform the division, and so on.
Solidity allows you to pass function types as parameters to other functions. In the previous example, we stored a function reference in a variable. Now, we will demonstrate how to pass functions directly as arguments using this syntax:
function foo(function () internal pure returns (uint) bar) internal pure returns (uint) {
return bar();
}
In this case, foo is a function that accepts another function (bar) as its argument. This function (bar) must match the signature: no parameters and returning a uint. Inside foo, we can call bar() directly.
We will now rewrite the previous dynamic calculator example where instead of assigning a function to a variable, we pass the function directly as an argument to another function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Calculator {
// Define basic arithmetic functions
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function subtract(uint a, uint b) internal pure returns (uint) {
return a - b;
}
function multiply(uint a, uint b) internal pure returns (uint) {
return a * b;
}
function divide(uint a, uint b) internal pure returns (uint) {
require(b > 0, "Cannot divide by zero");
return a / b;
}
// Function that accepts an operation function and calls it
function executeOperation(
uint a,
uint b,
function(uint, uint) internal pure returns (uint) operation
) internal pure returns (uint) {
return operation(a, b);
}
// Main function to handle different operations
function calculate(uint a, uint b, string memory operationType) public pure returns (uint result) {
// Pass the correct function directly to executeOperation
if (keccak256(bytes(operationType)) == keccak256(bytes("add"))) {
return executeOperation(a, b, add);
} else if (keccak256(bytes(operationType)) == keccak256(bytes("subtract"))) {
return executeOperation(a, b, subtract);
} else if (keccak256(bytes(operationType)) == keccak256(bytes("multiply"))) {
return executeOperation(a, b, multiply);
} else if (keccak256(bytes(operationType)) == keccak256(bytes("divide"))) {
return executeOperation(a, b, divide);
} else {
revert("Unknown operation type");
}
}
}
executeOperation Function: This function accepts two numbers (a, b) and a function (operation) that performs an arithmetic operation. The function signature is:
function(uint, uint) internal pure returns (uint)
This means the function passed must accept two uint parameters and return a uint. Inside executeOperation, we simply call the passed function (operation(a, b)).
Passing Functions Directly: In the calculate function, instead of assigning a function to a variable, we pass the function directly to executeOperation:
return executeOperation(a, b, add);
Here, add, subtract, multiply, or divide are passed directly as arguments.
Dynamic Execution: Based on the operationType string, the appropriate function is passed to executeOperation. This function is then called with the provided inputs (a, b).
In this syntax:
function foo(function () internal pure returns (uint) bar) internal pure returns (uint) {
return bar();
}
foo is a function that takes another function bar as an argument.
The parameter bar must be a function with the signature () internal pure returns (uint), meaning:
It takes no arguments.
It returns a uint.
It is an internal and pure function.
Inside foo, you simply call the function bar().
This syntax is very powerful because it allows you to write generic code where behavior (in the form of a function) can be injected and executed at runtime. It simplifies logic by removing the need for conditional function assignments and allows direct function execution.
Code Simplification: By passing functions directly as arguments, you reduce the need for temporary variables to store function references. It makes the code cleaner and more concise.
Flexibility: This pattern allows you to inject different logic dynamically into a function, making your code more modular and reusable.
Encapsulation: The logic for calling the function is encapsulated in the higher-order function (like executeOperation), making it easier to maintain and extend.
Function injection is useful when you need to write flexible, reusable code that can handle different logic paths without repeating code. Instead of writing separate logic for each operation or duplicating code, you can:
Dynamically choose a function at runtime.
Pass functions as parameters, making your code more modular and reusable.
Reduce code duplication, since you only need to write the core logic once.
In Solidity, function injection is a powerful tool for dynamically selecting and calling functions. By using function type variables, you can build flexible and modular contracts that adapt to different inputs and conditions. In our example, we saw how you can implement a dynamic calculator that switches between different arithmetic operations using this technique.
The key takeaway is that function type variables allow you to inject different behaviors into your code, reducing duplication and increasing flexibility. This technique is particularly useful in scenarios where your contract needs to adapt to different behaviors at runtime.
Feel free to experiment with this pattern to make your smart contracts more flexible and adaptable!

In Solidity, you can pass functions as arguments to other functions or store them in variables. This technique is often referred to as function injection or function pointers. It allows for greater flexibility, as you can dynamically choose which function to call based on certain conditions.
For beginners and intermediate developers, this can seem confusing at first because the syntax is a bit different from how we usually call functions. In this article, we'll break it down step-by-step with clear examples to help you understand how function injection works in Solidity.
Function injection in Solidity refers to the concept of passing a function as a parameter or storing a function in a variable so that it can be called dynamically later. This gives your code more flexibility, as you can choose which function to call based on different conditions.
In Solidity, you can declare a function type as a variable, and later assign it a specific function that matches the same signature (input parameters and return types). Once assigned, you can invoke that function just like any other function call.
Let's start with a simple example to understand how it works.
Imagine you are building a basic calculator contract, and you want to dynamically choose between different arithmetic operations like addition, subtraction, multiplication, and division based on user input.
Here’s how you could do that using function injection.
We’ll start by defining internal functions that perform the basic arithmetic operations (add, subtract, multiply, divide).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Calculator {
// Basic arithmetic functions
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function subtract(uint a, uint b) internal pure returns (uint) {
return a - b;
}
function multiply(uint a, uint b) internal pure returns (uint) {
return a * b;
}
function divide(uint a, uint b) internal pure returns (uint) {
require(b > 0, "Cannot divide by zero");
return a / b;
}
}
Each function takes two numbers (a and b) and returns the result of the respective operation.
Next, we'll create a function type variable to store a reference to any of the above functions. This variable will allow us to dynamically choose which operation to perform.
function (uint, uint) internal pure returns (uint) operation;
This line of code declares a variable operation that can hold any function matching the following signature:
Takes two uint parameters.
Returns a uint.
Is an internal and pure function.
Now, let's create a function that accepts an operation type (addition, subtraction, etc.) and uses function injection to call the correct function dynamically.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Calculator {
// Define basic arithmetic functions
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function subtract(uint a, uint b) internal pure returns (uint) {
return a - b;
}
function multiply(uint a, uint b) internal pure returns (uint) {
return a * b;
}
function divide(uint a, uint b) internal pure returns (uint) {
require(b > 0, "Cannot divide by zero");
return a / b;
}
// Use function injection to perform the desired operation
function calculate(uint a, uint b, string memory operationType) public pure returns (uint result) {
function (uint, uint) internal pure returns (uint) operation;
// Dynamically assign the correct operation based on input
if (keccak256(bytes(operationType)) == keccak256(bytes("add"))) {
operation = add;
} else if (keccak256(bytes(operationType)) == keccak256(bytes("subtract"))) {
operation = subtract;
} else if (keccak256(bytes(operationType)) == keccak256(bytes("multiply"))) {
operation = multiply;
} else if (keccak256(bytes(operationType)) == keccak256(bytes("divide"))) {
operation = divide;
} else {
revert("Unknown operation type");
}
// Call the injected function and return the result
return operation(a, b);
}
}
operation: This is the function type variable that can hold any internal function with the signature (uint, uint) -> uint.
String Matching: Based on the operationType string passed to the calculate function, the correct function (add, subtract, multiply, or divide) is assigned to operation.
Function Call: Once the function is assigned to operation, we can call it using operation(a, b), and it will return the result of the appropriate arithmetic operation.
Calling calculate to Add:
calculate(10, 5, "add") // returns 15
Calling calculate to Multiply:
calculate(10, 5, "multiply") // returns 50
Calling calculate to Divide:
calculate(10, 2, "divide") // returns 5
In Solidity, you can create variables that can store references to functions. These variables allow you to dynamically decide which function to call at runtime. In our example, operation is a variable that can hold any function that:
Is internal (i.e., can only be called inside the contract).
Is pure (i.e., it doesn’t read or modify the contract’s state).
Takes two uint parameters and returns a uint.
if (keccak256(bytes(operationType)) == keccak256(bytes("add"))) {
operation = add;
}
Here, we're checking the string passed in ("add", "subtract", etc.), and based on the result, we assign the corresponding function (add, subtract, multiply, or divide) to the operation variable.
Once we assign a function to the variable operation, we can call it like a normal function:
return operation(a, b);
This dynamically calls the correct function based on the input. If operation points to add, it will perform the addition. If it points to divide, it will perform the division, and so on.
Solidity allows you to pass function types as parameters to other functions. In the previous example, we stored a function reference in a variable. Now, we will demonstrate how to pass functions directly as arguments using this syntax:
function foo(function () internal pure returns (uint) bar) internal pure returns (uint) {
return bar();
}
In this case, foo is a function that accepts another function (bar) as its argument. This function (bar) must match the signature: no parameters and returning a uint. Inside foo, we can call bar() directly.
We will now rewrite the previous dynamic calculator example where instead of assigning a function to a variable, we pass the function directly as an argument to another function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Calculator {
// Define basic arithmetic functions
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function subtract(uint a, uint b) internal pure returns (uint) {
return a - b;
}
function multiply(uint a, uint b) internal pure returns (uint) {
return a * b;
}
function divide(uint a, uint b) internal pure returns (uint) {
require(b > 0, "Cannot divide by zero");
return a / b;
}
// Function that accepts an operation function and calls it
function executeOperation(
uint a,
uint b,
function(uint, uint) internal pure returns (uint) operation
) internal pure returns (uint) {
return operation(a, b);
}
// Main function to handle different operations
function calculate(uint a, uint b, string memory operationType) public pure returns (uint result) {
// Pass the correct function directly to executeOperation
if (keccak256(bytes(operationType)) == keccak256(bytes("add"))) {
return executeOperation(a, b, add);
} else if (keccak256(bytes(operationType)) == keccak256(bytes("subtract"))) {
return executeOperation(a, b, subtract);
} else if (keccak256(bytes(operationType)) == keccak256(bytes("multiply"))) {
return executeOperation(a, b, multiply);
} else if (keccak256(bytes(operationType)) == keccak256(bytes("divide"))) {
return executeOperation(a, b, divide);
} else {
revert("Unknown operation type");
}
}
}
executeOperation Function: This function accepts two numbers (a, b) and a function (operation) that performs an arithmetic operation. The function signature is:
function(uint, uint) internal pure returns (uint)
This means the function passed must accept two uint parameters and return a uint. Inside executeOperation, we simply call the passed function (operation(a, b)).
Passing Functions Directly: In the calculate function, instead of assigning a function to a variable, we pass the function directly to executeOperation:
return executeOperation(a, b, add);
Here, add, subtract, multiply, or divide are passed directly as arguments.
Dynamic Execution: Based on the operationType string, the appropriate function is passed to executeOperation. This function is then called with the provided inputs (a, b).
In this syntax:
function foo(function () internal pure returns (uint) bar) internal pure returns (uint) {
return bar();
}
foo is a function that takes another function bar as an argument.
The parameter bar must be a function with the signature () internal pure returns (uint), meaning:
It takes no arguments.
It returns a uint.
It is an internal and pure function.
Inside foo, you simply call the function bar().
This syntax is very powerful because it allows you to write generic code where behavior (in the form of a function) can be injected and executed at runtime. It simplifies logic by removing the need for conditional function assignments and allows direct function execution.
Code Simplification: By passing functions directly as arguments, you reduce the need for temporary variables to store function references. It makes the code cleaner and more concise.
Flexibility: This pattern allows you to inject different logic dynamically into a function, making your code more modular and reusable.
Encapsulation: The logic for calling the function is encapsulated in the higher-order function (like executeOperation), making it easier to maintain and extend.
Function injection is useful when you need to write flexible, reusable code that can handle different logic paths without repeating code. Instead of writing separate logic for each operation or duplicating code, you can:
Dynamically choose a function at runtime.
Pass functions as parameters, making your code more modular and reusable.
Reduce code duplication, since you only need to write the core logic once.
In Solidity, function injection is a powerful tool for dynamically selecting and calling functions. By using function type variables, you can build flexible and modular contracts that adapt to different inputs and conditions. In our example, we saw how you can implement a dynamic calculator that switches between different arithmetic operations using this technique.
The key takeaway is that function type variables allow you to inject different behaviors into your code, reducing duplication and increasing flexibility. This technique is particularly useful in scenarios where your contract needs to adapt to different behaviors at runtime.
Feel free to experiment with this pattern to make your smart contracts more flexible and adaptable!

Subscribe to Jumaru

Subscribe to Jumaru
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
No activity yet