Solidity programming: Transferring value between two Smart Contracts

Sam Vishwas
8 min readMar 1, 2023

--

Ethereum network allows for transfers to take place between different types of accounts, including two users, two contracts, a contract and user, or a user and contract. In this article, we’ll explore contract to contract scenario in detail and examine the different methods available in Solidity for transferring value between them.

Ethereum transferring from one account to another. PC: freepik.com

Transferring value on the Ethereum network requires careful consideration and several important steps. This guide covers everything you need to know about transferring ether (ETH), the native cryptocurrency of the network, between accounts using Solidity programming language.

When initiating a transfer, a transaction is created and sent to the network with information including the sender and recipient addresses, the amount of ether to be transferred, and the gas limit and price. In Solidity, wei is the unit of ether used for transfers, with 1⁰¹⁸ wei equal to 1 ether.

After the transaction is broadcasted to the network, it is validated and executed by nodes, with any associated smart contracts being executed. Once validated, the transaction is added to the network’s transaction pool, waiting to be mined by a miner.

When a miner successfully mines the block, the transaction is executed, and the ether is transferred from the sender’s account to the recipient’s account. The miner receives the gas fee associated with the transaction as a reward for including it in the block.

In summary, transferring ether on the Ethereum network involves creating and sending a transaction, validating, and executing it, and paying a gas fee to miners for processing the transaction. It’s important to consider all the factors involved in the transfer, such as gas prices and limits, to ensure a successful and cost-effective transfer between the desired accounts.

To learn more about sending and receiving ether in Solidity, visit the official Solidity documentation for detailed information.

Transferring value between two Smart Contracts:

Transferring value between smart contracts is a common task in Solidity. However, it’s important to be aware of the various methods available and the potential security risks involved in order to write safe and secure code.

In this article, we’ll look at how to transfer value between two smart contracts using Solidity, including examples of using transfer, send, and call. We'll also discuss best practices for error handling and security management to help prevent vulnerabilities such as DDoS and reentry attacks.

The Sample Contracts

To demonstrate value transfer between two smart contracts, we’ll use two simple contracts: Sender and Recipient.

transfer (2300 gas, throws error):

The Sender contract will have a function that allows it to transfer ether to the Recipient contract. In the Sender contract, the transferToRecipient function takes two parameters: the payableaddress of the Recipient contract and the amount of ether to transfer.

The transfer method is used to transfer ether from the Sender contract to the Recipient contract. If the transfer fails for any reasons including non-existing recipientAddress, an error is thrown and the ether transfer will not be executed, and any state changes that were made before the exception was thrown will be rolled back. Even better, a gas limit of 2300 is placed on the transfer method, which not only ensures that the transfer operation is completed within the gas limit but also acts as a safeguard against reentrancy attacks.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// Sender contract to send ether
contract Sender {
function transferToRecipient(address payable recipientAddress, uint amount) public {
require(balance >= amount, "Insufficient balance.");
// Transfer ether to the Recipient contract using transfer method
recipientAddress.transfer(amount);
}
}

Payable: Functions and addresses declared payable can receive ether into the contract.

The Recipient contract will have the required functionality to receive Ether and a function that allows it to withdraw the transferred ether.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// Recipient contract to receive ether
contract Recipient {

//--- Optional events to aid our debugging efforts.
event ReceivedWithData(address sender, uint256 amount, bytes data);
event ReceivedWithoutData(address sender, uint256 amount);
address payable public owner; // Set the owner to the creator of the contract
constructor() {
owner = payable(msg.sender);
}
// This function is marked as payable so that the contract can receive ether.
// It is also named "receive" because this is the function that is called
// when ether is sent to the contract without any function data.
receive() external payable {
//--- Avoid add any code here, but for debug purpose let us emit an event:
emit ReceivedWithoutData(msg.sender, msg.value);
}
fallback() external payable {
//--- Avoid add any code here, but for debug purpose let us emit an event:
emit Received(msg.sender, msg.value, msg.data);
}
// This function will allows the owner of this contract to
// transfer contract Ether to his/her own account.
function withdraw() external {
// Ensure that only the owner can call this function
require(msg.sender == owner, "Only the owner can withdraw.");
// Transfer the entire balance of this contract to the owner
uint256 balance = address(this).balance;
// Ensure that there is a non-zero blance to transfer
require(0 != balance, "There is no Ether to withdraw.");
owner.transfer(balance);
}
}

A contract receiving Ether must have at least one of the functions below

receive() external payable

fallback() external payable

receive() is called if msg.data is empty, otherwise fallback() is called.

Make sure that your funds do not get locked in your smart contract indefinitely. PC: coingeek.com

When developing smart contracts, it’s important to include the necessary functionality to prevent funds from becoming locked in the contract indefinitely. In the case of the Recipient contract, we have added a “withdraw” function to enable the recipient to access and use the funds transferred to them. This ensures that the funds are not permanently inaccessible and provides flexibility for the recipient to use them as intended.

When the transferToRecipient function is called, it will first check that the recipient contract has a payable function that can receive the ether. If the recipient contract does not have any payable function, the transfer will fail.

Therefore, it is essential to ensure that the recipient contract has a payable function, such as the receive or fallback function, to receive ether sent from the sender contract. Otherwise, the transfer will fail, and the transaction will be reverted.

send (2300 gas, returns bool):

The same Sender contract can be rewritten using the send method:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Sender {
function transferToRecipient(address payable recipient, uint256 amount) public {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = recipient.send(amount);
require(success, "Transfer failed");
}
}

In this implementation, the transferToRecipient function uses the send method to transfer the ether to the recipient address. Unlike the transfer method, the send method returns a boolean value indicating whether the transfer was successful or not. The send method also limits the amount of gas that can be used to 2300, similar to the transfer method.

Finally, the function checks that the transfer was successful using the boolean value returned by the send method and throws an error if it was not.

The difference between the transfer and send methods is that the transfer method throws an error if the transfer fails, while the send method returns a boolean value indicating whether the transfer was successful or not. Regarless of its retun value, any state changes that were made before the send method was called will still be saved to the blockchain, but the ether transfer will not be executed if returns false.

The send method can be used in situations where the consequences of a failed transfer are not severe and where it is desirable to have more control over the transaction flow. However, the transfer method is preferred in situations where the transaction flow is more straightforward and where it is important to ensure that the transfer succeeds or fails atomically.

call (forward all gas or set gas, returns bool):

The same Sender contract can be rewritten using the call method:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Sender {
function transferToRecipient(address payable recipient, uint256 amount) public {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, bytes memory data) = recipient.call{value: amount}("");
require(success, "Transfer failed");
}
}

In this implementation, the transferToRecipient function uses the call method to transfer the ether to the recipient address. The call method allows for more customization of the transaction, including specifying gas limits and passing arguments to the recipient contract. In this case, we pass an empty byte array as the data parameter to indicate that we are not passing any arguments to the recipient contract.

Like the send method, the call method returns a boolean value indicating whether the transaction was successful or not. In addition, it returns a bytes array containing any data that was returned by the recipient contract.

Finally, the function checks that the transfer was successful using the boolean value returned by the call method and throws an error if it was not.

Compared to the send method, the call method allows for more customization of the transaction, including specifying gas limits and passing arguments to the recipient contract. However, it is also more complex to use and can be more prone to errors if not used carefully. In addition, the gas cost of a call transaction can be higher than that of a send transaction due to the additional customization options available.

To specify gas limits in the call method we can use the following implementation:

(bool success, bytes memory data) = recipient.call{value: amount, gas: 2300}("");

In this implementation, we’ve added a gas parameter to the call method, specifying a gas limit of 2300. This is the same gas limit that is imposed on the transfer method in Ethereum.

By setting a gas limit of 2300, we are limiting the amount of gas that can be used by the recipient contract when receiving the ether transfer. This is a security measure to prevent reentrancy attacks, where a malicious contract could attempt to repeatedly call back into the Sender contract before the original transfer has completed, potentially stealing funds or causing other unexpected behavior.

It’s important to note that setting a gas limit of 2300 may not be appropriate for all contracts, and the gas limit should be carefully chosen based on the specific requirements of the contract and the potential risks involved. Additionally, it’s always important to thoroughly test contracts for potential security vulnerabilities, including reentrancy attacks, before deploying them to the Ethereum network.

Overall, the choice between the send and call methods depends on the specific requirements of the contract and the level of customization and control that is needed over the transaction flow.

Thank you for your interest in this topic, if you have any specific ask for my future articles then please feel free to drop me a message.

Continue your learning by visiting useful links like these….

https://docs.soliditylang.org/en/v0.8.19/

https://medium.com/coinmonks/solidity-transfer-vs-send-vs-call-function-64c92cfc878a

https://solidity-by-example.org/

Thanks ChatGPT for assisting me in writing this article.

--

--

Sam Vishwas
Sam Vishwas

Written by Sam Vishwas

Experienced software architect available for work. 25+ years of design & development experience. Blockchain enthusiast skilled in multiple languages.

No responses yet