Attack Vectors in Solidity #8: Denial of Service
Denial of Service as an attack vector in solidity (including gas limit reached, unexpected throw, unexpected kill, access control breached)
Table of contents
Introduction
A denial of service (DoS) attack is a type of cyberattack in which an attacker seeks to make a computer or network resource unavailable to its intended users by disrupting the services of a host connected to the Internet. In the context of Solidity, a programming language used for writing smart contracts on the Ethereum platform, DoS attacks can take various forms, including gas limit reached, unexpected throw, unexpected kill, and access control breached. In this article, we will discuss each of these attack vectors in detail, along with code examples and solutions.
- Gas limit reached:
Every smart contract on the Ethereum platform has a gas limit, which is the maximum amount of computation that can be performed before the contract execution is halted. If a contract consumes more gas than the gas limit, it will throw an exception and stop executing. This can be exploited by an attacker to launch a DoS attack by sending transactions to the contract with a high gas consumption, causing the contract to throw an exception and become unavailable to other users.
To prevent this type of attack, it is important for developers to carefully consider the gas consumption of their contract functions and set the gas limit appropriately. In addition, developers can use the Solidity "gas" keyword to specify the maximum gas consumption for individual functions. For example:
function foo(uint256 x) public payable gas(500000) {
// function code goes here
}
In this example, the function "foo" is defined with a maximum gas consumption of 500,000. If an attacker attempts to call this function with a transaction that consumes more than 500,000 gas, the transaction will fail and the contract will not be disrupted.
- Unexpected throw:
An unexpected throw is a type of DoS attack in which an attacker causes a contract to throw an exception by providing unexpected input to the contract. This can be done by calling a contract function with the wrong number or type of arguments, or by calling a function with malicious input that causes the contract to throw an exception. If a contract throws an exception, it will stop executing and become unavailable to other users.
To prevent this type of attack, developers can use Solidity's type system and error handling mechanisms to ensure that contract functions are only called with valid input. For example:
function foo(uint256 x) public {
require(x > 0, "x must be positive");
// function code goes here
}
In this example, the "require" statement checks that the input value "x" is positive before executing the rest of the function. If "x" is not positive, the function will throw an exception and stop executing.
- Unexpected kill:
An unexpected kill is a type of DoS attack in which an attacker causes a contract to self-destruct, or "kill" itself, by calling the contract's "suicide" function. This can be done by an attacker who has gained access to the contract's code, either by discovering a vulnerability in the contract or by obtaining the contract's private key. If a contract is killed, it will be permanently removed from the blockchain and become unavailable to all users.
To prevent this type of attack, developers can implement access control measures to restrict the ability to call the "suicide" function to authorized users. For example:
contract MyContract {
address public owner;
constructor() public {
owner = msg.sender;
}
function kill() public {
require(msg.sender == owner, "Only the owner can call this function");
selfdestruct(owner);
}
}
In this example, the "kill" function can only be called by the contract owner, as identified by the "owner" variable. This is accomplished using the "require" statement, which checks that the caller of the function (identified by the "msg.sender" variable) is the same as the owner. If the caller is not the owner, the function will throw an exception and stop executing.
- Access control breached:
Access control is a security measure used in smart contracts to restrict access to certain functions or variables to certain users. If an attacker is able to breach this access control and gain access to restricted functions or variables, they may be able to manipulate the contract in a way that causes it to behave unexpectedly, leading to a DoS attack. For example, an attacker may be able to call a function that was intended to be used only by the contract owner, causing the contract to throw an exception or behave in an unexpected way.
To prevent this type of attack, developers can use Solidity's access control keywords (such as "private," "internal," and "public") to specify the visibility of functions and variables. For example:
contract MyContract {
address public owner;
uint256 public balance;
constructor() public {
owner = msg.sender;
}
function setBalance(uint256 newBalance) public {
require(msg.sender == owner, "Only the owner can call this function");
balance = newBalance;
}
}
In this example, the "setBalance" function can only be called by the contract owner, as identified by the "owner" variable. The "balance" variable is declared as "public", meaning that it can be accessed by any contract or external actor. However, only the owner can modify the value of the "balance" variable through the "setBalance" function.
Other Best Practices:
In addition, there are several other best practices that developers can follow to mitigate the risk of DoS attacks in Solidity:
- Use the "view" and "pure" functions:
The "view" and "pure" functions in Solidity are functions that do not modify the state of the contract and do not have any external side effects (such as calling other contracts or sending Ether). These functions can be marked with the "view" or "pure" keyword to indicate that they are read-only and do not need to be included in the blockchain. This can help reduce the gas consumption of these functions and make them less vulnerable to DoS attacks.
- Avoid using loops:
Loops in Solidity can consume a large amount of gas, making them vulnerable to DoS attacks. To avoid this, developers can use alternative methods to achieve the same result, such as using recursive functions or the Solidity "assembly" keyword.
- Use the "assert" function:
The "assert" function in Solidity is used to test for conditions that should always be true. If the condition tested by "assert" is not true, the contract will throw an exception and stop executing. This can be used to prevent DoS attacks by ensuring that the contract functions only execute when given valid input.
- Use the "revert" function:
The "revert" function in Solidity is used to stop the execution of a contract function and revert any changes made to the contract state. This can be used to prevent DoS attacks by ensuring that the contract functions only execute when given valid input, and by allowing the contract to return to a known state if an error occurs.
- Use the "onERC20Received" function:
If your contract receives ERC20 tokens, you can use the "onERC20Received" function to handle incoming token transfers in a more gas-efficient way. This function allows you to process multiple token transfers in a single transaction, reducing the risk of DoS attacks.
By following these best practices, developers can help protect their Solidity contracts from DoS attacks and ensure that they remain available and functional for their intended users.
Conclusion
In conclusion, denial-of-service (DoS) attacks can be a significant threat to smart contracts written in Solidity. These attacks can take various forms, including gas limit reached, unexpected throw, unexpected kill, and access control breached. To mitigate the risk of DoS attacks, it is important for developers to implement appropriate security measures, such as setting appropriate gas limits, using error handling, and implementing access controls. In addition, developers can follow best practices such as using the "view" and "pure" functions, avoiding loops, using the "assert" and "revert" functions, and using the "onERC20Received" function to handle incoming token transfers. By understanding the various attack vectors and implementing these measures, developers can help protect their contracts from DoS attacks and ensure that they remain available and functional for their intended users.