From Solidity version 0.8.11 we can use an interface to define a function to pass to encodeCall parameters.
encodeCall
is a function that allows you to encode a function call with its parameters into a single byte array. This byte array can then be used to make a low-level call to another contract’s function.
Here is an example from the Solidity Snippets Github Repository: https://github.com/jamesbachini/Solidity-Snippets/blob/main/contracts/encodeCall.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.11;
interface IERC20 {
function transfer(address recipient, uint amount) external returns (bool);
}
contract EncodeCall {
function encodeCallData(address _to, uint _value) public pure returns (bytes memory) {
return abi.encodeCall(IERC20.transfer, (_to, _value));
}
}
This Solidity contract defines an interface called “IERC20” and a contract called “encodeCall”.
The “IERC20” interface defines the standard ERC20 “transfer” function which takes an address and an amount, then returns a boolean if it went through.
The “EncodeCall” contract contains a function called “encodeCallData” that takes two parameters: an address “_to” and an unsigned integer “_value”. This function uses the “abi.encodeCall” function to encode the function signature of the “transfer” function defined in the “IERC20” interface along with the parameters “_to” and “_value”. The resulting bytecode is then returned as a “bytes” variable.
The “abi.encodeCall” function is a low-level function in Solidity that is used to encode the function signature and arguments of a function call into a single bytecode. It takes two arguments: the first one is the function signature (in this case, IERC20.transfer), and the second one is an array of the function’s arguments.
We can also use the abi.encode() and abi.decode() functions to encode and pass data.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
contract Encoding {
uint public decoded = 0;
function encoding() public {
bytes memory data = abi.encode(888);
(decoded) = abi.decode(data, (uint));
}
}
It’s also possible to use encodeWithSignature to encode a function call but beware that this is more prone to errors and bugs than using an interface. Here is a full example with one contract calling the other.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
contract Add {
function add(uint _x, uint _y) public pure returns(uint) {
return _x + _y;
}
}
contract Caller {
function callMe(address _target, uint _x, uint _y) public returns (uint) {
bytes memory payload = abi.encodeWithSignature("add(uint256,uint256)", _x, _y);
(bool success, bytes memory result) = _target.call(payload);
require(success, "Test Transaction Failed");
(uint decoded) = abi.decode(result, (uint));
return decoded;
}
}
The “Add” contract contains a function called “add” that takes two unsigned integers (_x and _y) as input and returns their sum.
The “Caller” contract contains a function called “callMe” that takes three variables: an address (_target) that specifies the contract address of the “Add” contract, and two unsigned integers (_x and _y) that will be passed to the “add” function of the “Add” contract.
Inside the “callMe” function, the input variables _x and _y are encoded using the “abi.encodeWithSignature” function, which generates the function signature for the “add” function of the “Add” contract. The resulting bytecode is then passed as a parameter to the “call” function of the _target contract.
The “call” function executes the function whose bytecode is passed to it and returns a tuple of two values: a boolean value indicating whether the execution was successful or not, and the output of the function. The “require” statement then checks whether the execution was successful or not. If it failed, it throws an error message “Test Transaction Failed”.
If the execution was successful, the “abi.decode” function is called to decode the output of the “add” function from the “Add” contract. This function takes two arguments: the first one is the output returned by the “call” function and the second one is the data type of the output (uint). Finally, the decoded output value is returned by the “callMe” function.
I hope this information has proved of interest and it provides the confidence to use these low level calls with 3rd party protocols and build on the lego bricks of DeFi.