The Smart Contract Weakness Classification and Test Cases (SWC) Registry is a set of Web3 vulnerabilities to avoid when writing smart contracts. It may seem daunting to understand every issue so I’ll do my best to demystify every issue and explain each vulnerability with real-world examples. This write-up is intended for those who are just starting out in solidity development and smart contract auditing.
SWC-103: Unchecked Call Return Value
In this write-up, I will be going through these 5 bullet points. Take note of the first one because it is especially important to understand the vulnerability at hand.
In-depth Explanation
The Vulnerability - Unchecked Call Return Value
When to use the call function
Real-world Examples
Summary
In-depth Explanation
Let’s take a look at the code above. This is a contract called SendEther, and it has a function called sendViaCall. The function takes in a parameter called address payable _to, the payable keyword means that this _to address can accept ETH. The function itself also has a payable keyword which means that the function is able to process transactions with more than or equal to zero Ether. The function is set to public, which means that anyone can call.
Now, let’s look at the function itself. The comments state that “Call returns a boolean value indicating success or failure”. This comment is the crux of this vulnerability, which we will be exploring more later. The next line is quite confusing so let’s break it down together.
On the left side before the equal sign, there are two variables, bool sent and bytes memory data. When variables are on the left of the equal sign, it means that these are the values that the function on the right side is returning. Call functions always return two values, a boolean which indicates whether the call succeeds or not, and a data value with type bytes. Developers usually only care about the first value, whether the call succeeds or not, and it is important to check this value because if the call fails and is left unchecked, then there will be serious repercussions.
On the right side of the equal sign, there is a call function with a _to behind it. This means that the contract is attempting to interact with the _to address, which is passed in the parameter. Next, there is {msg.value}, which is the ETH value being sent from the contract to the _to address.
Lastly, there is a require check for the bool value with the comment “Failed to sent Ether”. The boolean value sent must return true from the call function in order for the function to execute successfully.
All in all, what the contract is trying to do is this:
Attempt to transfer {msg.value} amount from the contract itself to the _to address
Two outputs will be returned from the call function, a bool value, and a bytes value
The function checks whether the bool returns true
If the bool is true, the whole function will execute successfully
Sometimes, the boolean will return false because the contract may not have enough ETH to send to the _to address. Imagine if I want to withdraw some money out of a contract. I call sendViaCall and pass in my own contract address. However, if the contract does not have ETH inside, then the call will fail.
The Vulnerability - Unchecked Call Return Value
Sometimes, you will see this code instead.
This code is a replica of the one above without the require line. As you can see, the call return value is unchecked. As such, when the call fails, the function will still execute.
Why is it a problem? Imagine if the contract accounts for the balances of users. After the call, the contract then increases the balance of the user. However, if the call returns false, the contract does not check the false return and will continue increasing the balance of the user, which results in an inflation attack.
Always remember to check the return value when using call functions.
When to use the call function
The call function should only be used when attempting to transfer a native token. The native token is the token that is used on the main net that the contract is deployed on. If the contract is deployed on the Ethereum main net, then the native token is ETH. If the contract is deployed on the Polygon main net, then the native token is MATIC. Otherwise, use the transfer function instead. There are three reasons why call should be used instead of transfer when transferring the native token.
The withdrawer smart contract does not implement a payable fallback function.
The withdrawer smart contract implements a payable fallback function which uses more than 2300 gas units.
The withdrawer smart contract implements a payable fallback function which needs less than 2300 gas units but is called through a proxy that raises the call’s gas usage above 2300.
Reference: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
Real-life example
The above code is an actual instance of not checking the return value when using the call functionality. This code is written using assembly, but the core idea of checking return value still stands.
This SWC vulnerability is good as a first pass for low-risk or non-critical issues in smart contract auditing. If you are a smart contract auditor, you can almost always refer to this issue and provide the necessary feedback when auditing your client’s code.
Summary
Use call instead of transfer when transferring native token
Always check for the return value of call