12 Common Smart Contract Vulnerabilities and How To Address Them

Smart contracts provide a fundamental benefit by enabling us to verify the software that underlies a service and its actions without relying on trust in the service provider.

 

For a decentralized currency system to be adopted, it must be based on immutable rules, public auditability, and trust. This requires the use of secure and well-designed smart contracts.

 

For context, Redcurry smart contracts are the currency token (adhering to the ERC20 standard), on-chain reserve asset tracking contract (closely following ERC721 standard), and the Governor contract, which is responsible for issuing and redeeming the currency and enforcing other treasury rules.

 

Yet wherever there are smart contracts, there are also those trying to exploit them. Operating secretly behind the scenes are people looking for ways to take advantage of any existing vulnerabilities.

 

As we approach the product launch of Redcurry, we’ve taken great care to avoid common design errors, and vulnerabilities often encountered in smart contract development. Here, I outline the 12 most prevalent vulnerabilities and the steps we took to mitigate them, so you don’t fall victim to these when building smart contracts.

Vulnerability 1: Short Address Attack

The Ethereum Virtual Machine (EVM) vulnerability exploited in this attack relates to function call parameters in the ERC20 standard. For example, the transfer function requires two parameters: the recipient address and the number of tokens to be sent. However, the standard does not specify a minimum length for the recipient address parameter.

An attacker can take advantage of this vulnerability by sending a transaction with a short address parameter, causing unexpected behavior in the contract. The attacker may be able to send more tokens than intended or even permanently lock the contract.

Note:

From Solidity v0.5.0 this vulnerability was fixed and is no longer a concerne. See Solidity changelog.

Vulnerability 2: Integer Overflow/Underflow

Integer overflow and underflow are common issues in Solidity smart contracts, occurring when the result of an arithmetic operation exceeds the maximum or minimum value that can be stored in the data type. These issues affect uint and int data types and can result in unexpected behavior or loss of assets. An overflow occurs when the result exceeds the maximum value. In contrast, an underflow occurs when it falls below the minimum value, causing the value to wrap around to zero or the maximum value, leading to potential errors.

Recommendation:

Use the SafeMath library in Solidity to prevent integer overflow and underflow issues. The library provides functions for performing arithmetic operations that automatically check for these conditions and throw an exception if detected. Here’s an example of how to use the SafeMath library to prevent integer overflow.

See available SafeMath functions here

				
					
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract MyContract {
  using SafeMath for uint256;

  uint256 public myVariable;

  function myFunction(uint256 arg1, uint256 arg2) public {
    myVariable = arg1.mul(arg2);
  }
}
				
			

Vulnerability 3: Reentry Attack

The reentrancy issue is a security vulnerability in Solidity smart contracts that allows attackers to recursively call functions in the contract, potentially resulting in the loss of funds or other assets.

Reentrancy occurs when an attacker is able to repeatedly call a contract function before the previous call has finished executing. This can result in the attacker being able to manipulate the contract state and steal tokens.

In EVM, calls to functions of other smart contracts occur synchronously. That is, the calling code waits for the end of the execution of the external method before continuing its own work. This can cause the called contract to use the intermediate state of the calling contract by allowing the external contract to call back into the calling contract before it has finished executing. If the calling contract modifies its state after the external contract call, the attacker can potentially manipulate this state to their advantage.

Recommendation:

The easiest way to address this vulnerability is to use the nonReentrant modifier available in OpenZeppelin’s ERC20.


Note that because there is a single nonReentrant guard, functions marked as nonReentrant may not call one another. However, this can be worked around by making those functions private and then adding external nonReentrant entry points.


To prevent reentrancy attacks, one can also use the “Checks-Effects-Interactions” pattern, which starts with checking inputs and conditions, moves on to modifying state variables, and ends with interacting with external contracts. This pattern ensures that state modifications are complete before external interactions are allowed, preventing the reentrancy issue.


Another approach to preventing reentrancy is to use the required keyword to ensure that a function call is completed before proceeding with further execution. Developers can also use the Mutex pattern, which involves setting a flag to prevent recursive calls to a function, or implement a withdrawal pattern that ensures the contract has sufficient funds before making any external calls.

Vulnerability 4: Approval Exploits

The approval exploits vulnerability is a security issue that arises in ERC20 token contracts when a user approves a third-party contract to transfer an unlimited amount of tokens on their behalf. This allows the approved contract to transfer tokens from the user’s account to any other account without requiring further approval from the user.

 

The problem with this approach is that it allows the approved contract to transfer tokens at any time, even if the user has revoked their approval or the approved contract has been compromised by an attacker. As a result, an attacker can exploit this vulnerability by gaining control of the approved contract and initiating unauthorized token transfers from the user’s account to their own account.

Recommendation:

To mitigate the approval exploits vulnerability, ERC20 token contracts should implement the allowance mechanism in a way that limits the number of tokens that an approved contract can transfer on behalf of the user. This can be achieved by setting a maximum allowance that can be approved or by implementing a time-bound allowance that automatically expires after a specified period of time.

 

Developers can also implement additional security measures, such as requiring users to confirm each token transfer or using multi-factor authentication to verify token transfers.

Vulnerability 5: Approval Race Condition

The ERC20 approve method is subject to a race condition vulnerability in which an attacker can exploit the timing of two transactions to transfer more tokens than the user intended. This is because the approve method sets the approval amount for an external address to a specific value. The newly set approved amount is ignored if another transaction is initiated before the first transaction is confirmed.

 

Here is a possible attack scenario:

– Alice approved Bob to transfer N of her tokens by calling the approve method.

 

– Later, Alice decided to change the allowance to M, but before the transaction was confirmed, Bob called the transferFrom method to transfer N tokens.

 

– If Bob’s transaction is confirmed first, he can then transfer additional M tokens, resulting in a total of N+M tokens transferred without Alice’s consent.

 

The vulnerability is classified as a “front-running” attack and can result in the loss of funds or other assets. However, there are ways to mitigate this vulnerability.

Recommendation:

To mitigate the described attack, one can change the allowance from N to 0 and then from 0 to M. However, the token owner must ensure that the first transaction successfully changed the allowance from N to 0, which can be difficult to verify through standard Web3 API. An alternative approach is to approve token transfers only to verified smart contracts or trusted individuals. Advanced blockchain explorers like EtherCamp can also be used to analyze changes in contract storage and verify transaction outcomes.

 

Learn more about this vulnerability and mitigation here.

Vulnerability 6: Cross-Function State Dependency

The Cross-Function State Dependency vulnerability shares similarities with the reentry vulnerability. In particular, it arises when two functions depend on the same contract state, which can result in undesirable consequences when one function is called midway through the execution of another. Moreover, this vulnerability can also occur when both functions are called simultaneously, leading to potential race conditions that compromise the integrity of the smart contract.

Recommendation:

To address this issue, it is recommended that you apply the same preventive measures used to handle other similar first-race conditions. In particular, ensure that all state changes are completed before any ether transfer occurs. Adopting this approach, the smart contract will execute all the necessary state changes before transferring any ether.

Custom logic implementation in smart contracts is often the cause for additional vulnerabilities or just expensive decisions beyond those found in existing standards. Here are some common mistakes made.

Vulnerability 7: Incorrect Data Storage

A common mistake in smart contract development is using unoptimized memory data storage, which can result in higher transaction gas costs. Memory is a temporary data storage area that holds variables during contract execution and is cleared between function calls.

Recommendation:

For functions where arguments are used only for reading values, calldata is recommended instead. Calldata is a read-only data storage area that is used to hold function arguments passed to a contract, which can optimize gas cost and enforce immutability on input arguments.

 

As an example, compare there following two functions:

				
					function readValue (bytes memory _value) external {
    // read values from _value
}

// Set argument as calldata and optimize its gas cost while enforcing immutability on the input argument.
function readValue (bytes calldata _value) external {
    // read values from _value
}
				
			

Vulnerability 8: Not Sanitizing Function Parameters

This happens when a function parameter is not sanitized, causing a potential misconfiguration of the contract.

Recommendation:

We advise all parameters be sanitized before use. In this example, checking against zero-address to prevent misconfiguration of the contract is recommended.

 

As an example, compare these two following functions:

				
					function initialize(address _someone) external initializer {
    someone = _someone;
    emit Initialize(msg.sender, address(_someone));
}

// Makes sure contract cannot be misconfigured by passing in zero address. 
// note: other edge-cases should be considered and common require cheks added to modifier. 
function initialize(address _someone) external initializer {
    require(_someone != address(0), "0_ADDRESS");
    someone = _someone;
    emit Initialize(msg.sender, address(_someone));
}

				
			

Vulnerability 9: Not Implementing Access Control On Open Functions

In Solidity, external and public are function visibility specifiers used to determine that a function can be accessed by other contracts and users. Therefore, if those functions are state-changing (not read-only), it is important to protect the functions with access control modifiers like onlyOwner() or onlyRole().

Recommendation:

If a function has to be open to everyone, ensure the internal functionality is not sensitive to malicious actors who might want to exploit the contract by using access control and input-checking modifiers.

Vulnerability 10: Assigning Role To Itself

In certain cases, when business logic demands, it may be required to have the functionality to assign a role to itself.

 

As an example:

				
					function initialize(address _admin) external initializer {
    require(_admin != address(0), "0_ACCOUNT");

    // `VALIDATOR_ADMIN` can admin each other in addition to
    // `VALIDATOR`s
    _setRoleAdmin(VALIDATOR_ADMIN, VALIDATOR_ADMIN);
    _setRoleAdmin(VALIDATOR, VALIDATOR_ADMIN);

}

				
			

When an individual assigns themselves the role of a VALIDATOR_ADMIN, they become the administrator of that role, granting them the ability to remove any other VALIDATOR_ADMIN and set the VALIDATOR role according to their own preference, all within a single transaction. This means that a single individual can gain complete control over the VALIDATOR_ADMIN role and exercise their power arbitrarily.

Recommendation:

We recommend that, if permitted by the business requirements, the VALIDATOR_ADMIN role be overseen by a supervisor role created temporarily and then removed from the system prior to the system’s activation.

Vulnerability 11: Embedding Proofs In Calldata

A function requiring proof of association with an off-chain entity may be vulnerable to another address submitting the same evidence before the original submitter due to transaction submission delays between submitting a transaction and that transaction reaching the network.

 

If this situation only pertains to informative operations, it may lead to ambiguity in the event of inconclusive proof. However, suppose the proof is utilized to verify the caller’s identity and determine access control. In that case, it can potentially result in the approval of malicious addresses (as data in a blockchain is public).

Recommendation:

The use of the commit-and-reveal approach can address this vulnerability. It is a cryptographic technique used to ensure the integrity of information in situations where secrecy and authenticity are both critical. It is commonly used in blockchain applications and other decentralized systems to protect sensitive data and prevent cheating or fraud.

 

The commit and reveal approach involves two steps – committing and revealing:

 

1.) In the commit step, a user generates a cryptographic hash of the information one wishes to protect, such as a secret value or a transaction. The hash is then submitted to the network or shared with other parties. This serves as a commitment to the information without actually revealing its contents.

 

2.) In the reveal step, the user provides the original information, which is then verified against the hash to ensure that it has not been tampered with or altered in any way. If the information matches the hash, it is considered authentic and can be accepted as valid.

 

The commit and reveal approach is particularly useful in situations where the integrity of the information is critical but cannot be revealed until a later time.

Vulnerability 12: Falsely Implying a Security Guarantee

This happens when a method, for example, a modifier, implies a security guarantee but, in reality, only enforces rules after execution and thus can lead to misuse.

 

As an example, the following modifier performs an execution before the required check is performed:

				
					modifier onlyTier(address account_, uint256 minimumTier_) {
    require(account_ != address(0), "0_ACCOUNT");
    someOperation(account_)
    require(isTier(account_, minimumTier_), "MINIMUM_TIER");
    someOtherOperation(account_)
}

				
			

Recommendation:

To avoid falsely implying a security guarantee, we recommend either enforcing a check at both the beginning and end of execution or renaming the function.

Closing remarks

It is crucial to note that the list provided is not exhaustive, and that you conduct comprehensive security testing on your contracts to identify potential vulnerabilities. 

Additionally we recommend to adhere to best practices in contract design and development, conduct cross-check code commits among engineers, run unit tests that cover various scenarios, including edge cases and subject your smart contracts  to specialized audits and stress-tests.

Sources

Rain Protocol Security Audit (Omniscia)

 

Tokenized Vaults Security Audit (Omniscia)

 

Security Documentation (OpenZeppelin)

 

Ethereum EIPs (GitHub)

 

Thether Smart COntract Code (Etherscan):