Intro to Smart Contract Security Audit: DOS



Denial of service (DoS) is a problem in smart contract security that is similar to traditional network security.

Prior Knowledge

Traditional network security denial of service (DoS): DoS is an abbreviation for denial of service. A denial of service occurs when an interference to a service reduces or eliminates its availability. The following are examples of common denial-of-service attacks against network protocols: SYN Flood, IP Spoofing, UDP Flood, Ping Flood, Teardrop Attack, LAND attack, Smurf attack, Fraggle attack, and so on.

Smart contract denial-of-service attack: A security issue that can result in code logic errors, compatibility issues, or excessive call depth (a feature of blockchain virtual machines), causing smart contracts to fail to function properly. Smart contract denial of service attack methods are relatively simple, including but not limited to the following three:

  • Denial of service attack based on code logic: This type of denial of service attack is typically caused by inaccuracy of the contract’s code logic. The most common example occurs when there is logic in the contract that loops through the incoming mapping or array. When there is no length limit on the incoming map or array, an attacker can consume a large amount of Gas by passing in a super-long map or array for loop traversal, causing the transaction’s Gas to overflow and eventually render the smart contract inoperable.
  • Denial of service attack based on external call: This denial of service attack is caused by the contract’s improper handling of external calls. For example, in a smart contract, there is a knot that changes the contract state based on the execution of an external function but doesn’t deal with the fact that the transaction failed. With this capability, an attacker can cause a transaction failure on purpose, and the smart contract will keep trying to process the same invalid data. Since the smart contract logic card cannot be used further in this environment, the smart contract is rendered either temporarily or permanently ineffective.
  • Operation management-based denial of service attack: In smart contracts, for instance, the Owner account frequently serves as the administrator, with extensive privileges. When the Owner role fails or the private key is lost, the transfer function, for instance, is vulnerable to a non-subjective denial of service attack, which could result in, for instance, the enabling or suspending of the transfer function.

Vulnerability Example

In my opinion, thanks to common background knowledge, everyone is familiar with the concept of a denial of service attack. The external call denial of service attack is the most common type of the three types of DOS attacks. In order to provide a thorough introduction, we will walk you through a typical code example below:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract KingOfEther {
address public king;
uint public balance;

function claimThrone() external payable {
require(msg.value > balance, "Need to pay more to become the king");

(bool sent, ) ={value: balance}("");
require(sent, "Failed to send Ether");

balance = msg.value;
king = msg.sender;

Vulnerability Analysis

We can see from the above contract that the goal is to select the “King of Ether.” The claimThrone() contract allows users to compete for the title of “King of Ether” by entering any amount of Ether greater than the previous user. If the coin value is higher than the previous player’s ETH, the ETH will remain in the contract and the new player will be crowned “Ether King,” while the old player’s ETH will be returned to them in the same fashion.

We can see that the logic of generating the new king and returning the old king is completed in the same function, and the refund return value is also checked in claimThrone(). Let’s combine these features to complete the attack.

Attack Contract

Note: The following attack scenarios and contract code logic are merely examples and are for demonstration purposes only.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Attack {
KingOfEther kingOfEther;

constructor(KingOfEther _kingOfEther) {
kingOfEther = KingOfEther(_kingOfEther);

function attack() public payable {
kingOfEther.claimThrone{value: msg.value}();

Let’s start by first analyzing the attack process:

  1. Alice deploys the KingOfEther contract.
  2. Alice calls KingOfEther.claimThrone() to send 1 ether to the KingOfEther contract to become the “King of Ether”.
  3. Bob calls KingOfEther.claimThrone() to send 2 ethers to the KingOfEther contract to become the new king.
  4. Alice receives a refund of 1 ETH.
  5. Eve uses the address of KingOfEther to deploy the attack contract.
  6. Eve calls Attack.attack() to send 3 ether to the KingOfEther contract.
  7. The Attack contract becomes the new king.
  8. Bob is unsatisfied with the outcome, so he calls KingOfEther.claimThrone() again, this time sending 20 ether to the KingOfEther contract to demonstrate his “financial competence.”
  9. Bob discovers that his transactions have been reversed, and he is no longer the new king. So far, Eve’s attack has rendered the KingOfEther contract permanently invalid, and the Attack contract has become the eternal “King of Ether.”

The wealthy and attractive Bob finds this frustrating; if he is so wealthy, why can’t he be king?

Let’s see why.

When Bob calls KingOfEther.claimThrone() to send 20 ethers to the KingOfEther contract, the refund logic of KingOfEther.claimThrone() will be triggered, and Eve’s 3 ethers will be returned to the Attack contract. Let’s examine the Attack contract once more. This contract does not implement the fallback() method of payable, so it cannot receive ether. As a result, the refund logic of KingOfEther.claimThrone() will always fail, and its return value will always be false. (sent, “Failed to send Ether”) checks are consistently reversed. As long as a refund is triggered, after the KingOfEther contract relays the Attack contract, no one can become the new king. Therefore, Eve executed a successful denial of service attack.

Suggestions for Repair

As a developer:

  1. Attention should be paid in the development of smart contracts to deal with consistent failures, such as asynchronous processing of potentially failing external call logic.
  2. When using Call to make external calls, loops, and traversals, keep an eye on the gas consumption.
  3. Avoid over-authorization of a single role. A reasonable division of permissions should be achieved when dealing with contract permissions, and multi-signature wallet management should be used for roles with permissions to prevent permission loss due to private key leakage.

The following is an example of a fix for the previously mentioned vulnerable contract:

// SPDX-License-Identifier: MITpragma solidity ^0.8.13;
contract KingOfEther { address public king; uint public KingValue; mapping(address => uint) public balances;
function claimThrone() external payable { balances[msg.sender] += msg.value;
require(balances[msg.sender] > balance, "Need to pay more to become the king"); KingValue = balances[msg.sender]; king = msg.sender; }
function withdraw() public { require(msg.sender != king, "Current king cannot withdraw");
uint amount = balances[msg.sender]; balances[msg.sender] = 0;
(bool sent, ) ={value: amount}(""); require(sent, "Failed to send Ether"); }}

The balances map has been added to the repair contract, which records the total amount of ether put into the contract by each person. In comparison to the previous contract, the player can now add ether to reclaim the throne after losing it. The key point of the repaired version is that there is a method that handles the refund logic asynchronously. To receive a refund, players must manually call withdraw(). Even if a malicious player refuses to accept ether, this has no effect and will not result in the previously mentioned denial of service.

As an auditor:

  • Analysis of internal contracts:
  1. Determine if the contract contains any logical errors that affect its usability.
  2. Pay close attention to the possibility of a DoS resulting from the excessive depth of virtual machine calls (1024 is the maximum depth).
  3. Concentrate on whether the logic of the code consumes a great deal of Gas.
  • Analysis of external contracts:
  1. Pay close attention to compatibility issues when interacting with external contracts, such as: the return value compatibility of TRC20-USDT isn’t processed, which results in tokens being locked.
  2. Check whether the external contract call’s return value corresponds to the expected result.
  • Analysis of rights management:

All function methods’ visibility and access rights must be examined and confirmed during the audit. It is necessary to combine the design documents provided by the project party in order to confirm the rights are in accordance with the design document descriptions during the audit. If it is determined that there are excessive authorizations or an unclear division of authority, it is essential to communicate with the project’s team about improving their processes and methods to ensure administrative and operational errors are prevented when the contract is in effect.

Leave a Reply