October 26th, 2022
This is the third article in a three-part series exploring vulnerabilities that place DeFi projects at risk. Click here to read DeFi Security Part 1 and here to read DeFi Security Part 2.
DeFi smart contracts are high-value targets for attackers. However, the fields of smart contract development and DeFi are relatively young. As a result, there is a limited number of experienced developers, and some smart contracts may be written by people without a clear understanding of potential security risks and best practices.
Smart contracts running on the Ethereum platform alone can contain a wide range of exploitable vulnerabilities, and several blockchains support smart contracts. These are some of the most common smart contract vulnerabilities that impact the security of DeFi projects.
Arithmetic vulnerabilities, such as integer overflows and underflows, arise from the fact that integer variable types have a fixed size in memory. These size constraints limit the range of values that each variable type can hold.
Integer overflow vulnerabilities occur when the value to be stored in a variable exceeds its maximum value, causing it to “roll over” and be interpreted as a much smaller value. Similarly, integer underflow vulnerabilities occur when the value to be stored falls below the minimum value, causing it to be interpreted as a smaller value. These events can occur as a result of arithmetic operations (addition, subtraction, multiplication, etc.) or unsafe typecasts (int to uint, etc.).
Arithmetic vulnerabilities are a significant problem for DeFi smart contracts because they can allow an attacker to bypass checks on the validity of an operation. For example, the statement require(balances[msg.sender] – amount > 0) is problematic with unsigned integers as the result of the subtraction will always be interpreted as a positive number.
The ERC-20 standard defines the core functions that ERC-20 token contracts should implement. This includes their expected behavior and data types, including their return values and how they handle errors.
Not all smart contracts follow the ERC-20 standard’s requirements for error handling. A smart contract function can indicate an error in one of two ways:
If a called function throws an exception, the caller can handle the error by allowing the transaction to revert, which requires no error handling code. However, a return value of false doesn’t cause reversion, and a failure to check return values could result in the caller continuing to run while incorrectly assuming that a transfer succeeded.
In the DeFi space, problems arise when a token contract returns false upon a failed token transfer and a DeFi contract assumes that it will revert. This could allow an attacker to intentionally make a failed deposit into a contract and receive reward tokens in return.
In Solidity, transfers of Ether to a smart contract trigger its fallback function. This allows the contract to execute some code, such as updating its internal balance or sending reward tokens in response.
A fallback function can also be used to revert unwanted token transfers. This could be used to exploit vulnerable sending contracts or to manage the smart contract’s balance.
However, if a contract relies on its ability to revert transactions (i.e. has logic that tests if the contract’s balance is strictly equal to a value), then it can be vulnerable to attack. Ether can be forced into a contract by a self-destructing contract, by sending block rewards to its address, or by sending value to a predictable contract address before the contract is deployed.
Reentrancy is one of the most famous smart contract vulnerabilities on the Ethereum platform. It was the cause of the infamous DAO hack and many other high-value hacks since.
Reentrancy attacks take advantage of functions that perform the following steps in this order:
This logic flow is problematic because of the existence of fallback functions in Solidity.
At step 2, sending Ether to a smart contract will trigger its fallback function, allowing it to run code. If the fallback function calls the vulnerable function, it reenters before the state update in step 3 occurs. As a result, the attacker can perform a second, invalid withdrawal because the validation in step 1 uses an outdated value.
Reentrancy vulnerabilities can be avoided by using the check-effects-interaction pattern (swapping steps 2 and 3). Performing state updates before the transfer ensures that the values used in step 1 are up-to-date if a malicious contract attempts to reenter the function.
As mentioned previously, Solidity allows smart contract functions to indicate errors in a couple of different ways. They can either throw an exception or return false.
This becomes problematic if the calling function assumes that a called function will revert. If the function returns false and the caller doesn’t check the return value, then the caller may continue running with an invalid state.
The tx.origin value holds the address that performed a given transaction. This includes calls to smart contract functions, which may use this value for access control.
However, using tx.origin for access management can leave a smart contract vulnerable to attack. If an attacker can induce a trusted smart contract to make the call on their behalf, then the trusted contract’s address is the one that will be stored in tx.origin, allowing the attacker to bypass access controls and access restricted functionality.
This list only scratches the surface of the types of vulnerabilities that can exist in a DeFi smart contract. In addition to these and other general smart contract vulnerabilities, DeFi contracts may also contain DeFi-specific vulnerabilities, such as price oracle manipulation.
A smart contract security audit is the best way to identify and remediate vulnerabilities in a smart contract before deploying it to the blockchain. To learn more, reach out to our Web3 security experts at firstname.lastname@example.org.