How to use call.value
Recently someone asked me about the difference between transfer() and call.value. Why people were talking about it now? Well, it happened because of Istanbul hard fork in 2019. Gas cost of the SLOAD operation increased, causing a contract’s fallback function to cost more than 2300 gas. So everybody should stop using .transfer()
and .send()
and instead use .call()
. More information you can have at this very good repo, Secure Development Recommendations, maintained by ConsenSys Diligence: https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/#dont-use-transfer-or-send.
You can easily find a bunch of examples of how to use this new standard, ig, here and here.
But let's talk about one specific topic: the payload. All examples have just a simple ether transfer, msg.sender.call{value: amount}("")
. What is calling some attention is the 2nd parameter, the empty string ""
. I am going to explain how to use it.
First, let's check how a very simple withdraw() function works:
function withdraw(uint256 amount) external {
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
}
But what happens if you would like to send ether to another smart contract? It is almost the same. In my simple example, Bank is a smart contract that has a payable fallback function.
function deposit(address bank) external payable {
(bool success,) = bank.call{value: msg.value}("");
require(success, "Transfer failed.");
}
So far so good. Now you would like to send ether to a function called deposit() from Bank smart contract. How to do it? It is also pretty simple. But because of the lack of information, I decided to explain it with more details.
function makeDeposit(address bankAddress) public payable {
bytes32 functionHash = keccak256("deposit()");
bytes4 function4bytes = bytes4(functionHash);
bytes payload = abi.encode(function4bytes);
if (msg.value > 0) {
(bool success,) = bank.call{value: msg.value}(payload);
require(success, "Ether transfer failed.");
}
}
- functionHash is going to be equal to 0xd0e30db03f2e24c6531d8ae2f6c09d8e7a6ad7f7e87a81cb75dfda61c9d83286 . The part that is in bold is just to highlight the next step.
- We just need the first 4 bytes from the function hash, so function4bytes is equal to 0xd0e30db0.
- payload is the result that we want to be used inside call.value. And it is equal to 0xd0e30db000000000000000000000000000000000000000000000000000000000
I hope that writing in 3 steps you can better understand what is needed to be able to send ether to another smart contract.
Here is a working example so you can try and test using Remix. You can use the github version or the code pasted here. Have fun.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;/**
* Simple example of how to use the payload when using call.value
**/
contract Payloadtest {
bytes32 public functionHash;
bytes4 public function4bytes;
bytes public payload ;
/**
* It sends the value to the Bank smart contract
* calling deposit() function.
* It shows that seding the 4 bytes of the hash of
* deposit() as the payload is like calling
* the deposit() function from Bank contract
* @param bank Bank contract address
*/
function makeADeposit(address bank ) public payable
{
functionHash = keccak256("deposit()");
function4bytes = bytes4(functionHash);
payload = abi.encode(function4bytes);
if (msg.value > 0) {
(bool success,) = bank.call{value: msg.value}(payload);
require(success, "Ether transfer failed.");
}
}
/**
* It sends the value to the Bank smart contract.
* Since payload is empty, it
* is like sending value to the smart contract.
* @param bank Bank contract address
*/
function makeATransfer(address bank ) public payable
{
if (msg.value > 0) {
(bool success,) = bank.call{value: msg.value}("");
require(success, "Ether transfer failed.");
}
}
}contract Bank {
uint256 public totalDeposit;
event Received(address, uint256);
/// @dev Updates totalDeposit
function deposit() public payable
{
totalDeposit += msg.value;
}
/// @dev returns the Bank contract balance
function balance() external view returns(uint256)
{
return address(this).balance;
}
/// @dev This is the function that is executed on
/// plain Ether transfers
receive() external payable {
totalDeposit += msg.value;
emit Received(msg.sender, msg.value);
}
}