Denial of Service (DoS)

This category is very broad, but fundamentally consists of attacks where users can render a contract inoperable for a period of time, or in some cases permanently. This can trap ether in these contracts forever, as was the case in Real-World Example: Parity Multisig Wallet (Second Hack).

The Vulnerability

There are various ways a contract can become inoperable. Here we highlight just a few less-obvious Solidity coding patterns that can lead to DoS vulnerabilities:

Looping through externally manipulated mappings or arrays

This pattern typically appears when an owner wishes to distribute tokens to investors with a distribute-like function, as in this example contract:

  1. contract DistributeTokens {
  2. address public owner; // gets set somewhere
  3. address[] investors; // array of investors
  4. uint[] investorTokens; // the amount of tokens each investor gets
  5. // ... extra functionality, including transfertoken()
  6. function invest() external payable {
  7. investors.push(msg.sender);
  8. investorTokens.push(msg.value * 5); // 5 times the wei sent
  9. }
  10. function distribute() public {
  11. require(msg.sender == owner); // only owner
  12. for(uint i = 0; i < investors.length; i++) {
  13. // here transferToken(to,amount) transfers "amount" of
  14. // tokens to the address "to"
  15. transferToken(investors[i],investorTokens[i]);
  16. }
  17. }
  18. }

Notice that the loop in this contract runs over an array that can be artificially inflated. An attacker can create many user accounts, making the investor array large. In principle this can be done such that the gas required to execute the for loop exceeds the block gas limit, essentially making the distribute function inoperable.

Owner operations

Another common pattern is where owners have specific privileges in contracts and must perform some task in order for the contract to proceed to the next state. One example would be an Initial Coin Offering (ICO) contract that requires the owner to finalize the contract, which then allows tokens to be transferable. For example:

  1. bool public isFinalized = false;
  2. address public owner; // gets set somewhere
  3. function finalize() public {
  4. require(msg.sender == owner);
  5. isFinalized == true;
  6. }
  7. // ... extra ICO functionality
  8. // overloaded transfer function
  9. function transfer(address _to, uint _value) returns (bool) {
  10. require(isFinalized);
  11. super.transfer(_to,_value)
  12. }
  13. ...

In such cases, if the privileged user loses their private keys or becomes inactive, the entire token contract becomes inoperable. In this case, if the owner cannot call finalize no tokens can be transferred; the entire operation of the token ecosystem hinges on a single address.

Progressing state based on external calls

Contracts are sometimes written such that progressing to a new state requires sending ether to an address, or waiting for some input from an external source. These patterns can lead to DoS attacks when the external call fails or is prevented for external reasons. In the example of sending ether, a user can create a contract that does not accept ether. If a contract requires ether to be withdrawn in order to progress to a new state (consider a time-locking contract that requires all ether to be withdrawn before being usable again), the contract will never achieve the new state, as ether can never be sent to the user’s contract that does not accept ether.

Preventative Techniques

In the first example, contracts should not loop through data structures that can be artificially manipulated by external users. A withdrawal pattern is recommended, whereby each of the investors call a withdraw function to claim tokens independently.

In the second example, a privileged user was required to change the state of the contract. In such examples a failsafe can be used in the event that the owner becomes incapacitated. One solution is to make the owner a multisig contract. Another solution is to use a time-lock: in the example given the require on line 5 could include a time-based mechanism, such as require(msg.sender == owner || now > unlockTime), that allows any user to finalize after a period of time specified by unlockTime. This kind of mitigation technique can be used in the third example also. If external calls are required to progress to a new state, account for their possible failure and potentially add a time-based state progression in the event that the desired call never comes.

Note

Of course, there are centralized alternatives to these suggestions: one can add a maintenanceUser who can come along and fix problems with DoS-based attack vectors if need be. Typically these kinds of contracts have trust issues, because of the power of such an entity.

Real-World Examples: GovernMental

GovernMental was an old Ponzi scheme that accumulated quite a large amount of ether (1,100 ether, at one point). Unfortunately, it was susceptible to the DoS vulnerabilities mentioned in this section. A Reddit post by etherik describes how the contract required the deletion of a large mapping in order to withdraw the ether. The deletion of this mapping had a gas cost that exceeded the block gas limit at the time, and thus it was not possible to withdraw the 1,100 ether. The contract address is 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3, and you can see from transaction 0x0d80d67202bd9cb6773df8dd2020e719 0a1b0793e8ec4fc105257e8128f0506b that the 1,100 ether were finally obtained with a transaction that used 2.5M gas (when the block gas limit had risen enough to allow such a transaction).