Solidity Security By Example #13: Double Spending #2

Phuwanai Thummavet
Valix Consulting
Published in
5 min readJun 13, 2023

By Phuwanai Thummavet

Smart contract security is one of the biggest impediments to the mass adoption of the blockchain. For this reason, we are proud to present this series of articles regarding Solidity smart contract security to educate and improve the knowledge in this domain to the public.

Double spending can happen due to improper smart contract design or incorrect implementation. Most of the time, the double spending vulnerability could be challenging to detect.

This article will explain how a smart contract vulnerable to double spending can be attacked and how to remediate the issue. Enjoy reading. 😊

You can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/13_double_spending_02.

Disclaimer:

The smart contracts in this article are used to demonstrate vulnerability issues only. Some contracts are vulnerable, some are simplified for minimal, some contain malicious code. Hence, do not use the source code in this article in your production.

Nonetheless, feel free to contact Valix Consulting for your smart contract consulting and auditing services.🕵

Table of Contents

  • The Vulnerability
  • The Attack
  • The Solution
  • Summary

The Vulnerability

The following code shows the InsecureNaiveBank contract demonstrating a simple (and naive) on-chain banking system. The contract allows anyone to deposit their Ether funds through the deposit function (lines 24–33).

The deposited Ethers can be withdrawn anytime via the withdraw function (lines 35–41).

A banker executes the applyInterest function (lines 45–56) to calculate compound interests for all depositors. The interest rate is fixed at 5% (line 4). 🤑🤑🤑

Besides, a banker can deposit Ether funds into the contract by invoking the depositBankFunds function (lines 20–22).

No doubt, the InsecureNaiveBank contract is vulnerable. Can you catch up on any issues? 👀

We would like to note that the InsecureNaiveBank contract got an unbounded denial-of-service issue on the applyInterest function.

But, it is not in the scope we will focus on in this article 🤔. Nonetheless, we already published the article explaining that denial-of-service issue in case you might be interested. 🤗

The InsecureNaiveBank contract got a design flaw in the deposit function (lines 24–33).

In detail, the deposit function will treat the function caller as a new depositor if the caller’s deposit balance tracked by the mapping userBalances is zero (line 28) — i.e., the if (userBalances[msg.sender] == 0) { … } statement.

Later, the new depositor will be registered into the contract by appending their address to the array userAddresses at the tail (line 29) — i.e., the userAddresses.push(msg.sender); statement. 🤔

The root cause of the vulnerability resides in the improper logical check of the new depositor in line 28. Consider the below figure to understand how the improper logical check in question can lead to a double spending attack. 👽

Figure 1. How an attacker performs a double spending attack for their BIG PROFIT

An attacker first executes the deposit function to deposit Ether funds (Step 1.1). Since the attacker’s deposit balance (tracked by the mapping userBalances[address(attacker)]) is zero, the attacker’s address will be registered as a new depositor (Step 1.2).

Next, the attacker empties their account by calling the withdraw function to withdraw all Ether funds (Step 2). As a result, the attacker’s deposit balance (the mapping userBalances[address(attacker)]) would become zero.

With Step 2, the attacker can bypass the improper logical check of the new depositor in line 28 easily. 😼

Later, the attacker executes Steps 1 and 2 multiple times at will to perform a double spending attack. For each iteration, the same attacker’s address will be registered to the array userAddresses (line 29). 😎

Once a banker 🤢 invokes the applyInterest function (lines 45–56) to compute the compound interests of all depositors (Step 3), the function will loop over the array userAddresses and update the compound interest of each depositor (lines 46–55).

For this reason, the attacker would receive the more significant interest as much as how many times they execute the above-described Steps 1 and 2. 🎃

The Attack

The code below exhibits the Attack contract that an attacker can use to exploit the InsecureNaiveBank contract.

To exploit the InsecureNaiveBank, an attacker executes the attack function (lines 18–29) and supplying 1 Ether as initial attack funds.

An attacker must also specify the parameter _xTimes — the number of times the attacker would like to double register their address (i.e., Steps 1 and 2 described in Figure 1 above).

Figure 2. The attack result

The result of the attack is shown in Figure 2. As you can see, the attacker achieved executing the 20-time double-spending attack.

With the same amount of 1 Ether deposited, the user0 received only 0.05 Ether interest 🥺.

Meanwhile, the attacker could gain a profit of over 1.65 Ethers. 💰💰💰

The Solution

The below FixedNaiveBank contract is the improved version of the InsecureNaiveBank contract. 👨‍🔧

To remediate the double spending issue on the deposit function, we have to redesign the way to verify the new depositor.

We introduced the new state variable named registered in line 9 to keep track of every registered account.

The registered variable will be used in line 35 in the deposit function — i.e., the if (!userAccounts[msg.sender].registered) { … } statement — for verifying whether a caller of the deposit function is the new depositor or not, instead of using the caller’s deposit balance (tracked by the mapping userBalances[msg.sender] as doing in the InsecureNaiveBank contract).

Once the new depositor is registered, the registered variable corresponding to that depositor account will be set to true (line 38).

Hence, an attacker would no longer execute the double spending attack on the FixedNaiveBank contract.

Note that:

Please do not use the FixedNaiveBank contract in your production. The FixedNaiveBank contract is just a proof of concept of how to remediate the double spending issue only but it still implants some security issues.

For instance, the approach to calculating the depositors’ interests is still insecure. In case you might be interested, however, please head over to our article explaining the denial-of-service issue. 🤗

Summary

In this article, you have learned how the improper logical check in a smart contract can lead to a double spending attack.

You have understood how an attacker exploits the vulnerable contract and how to fix the issue. We hope you enjoy reading our article. Until next time.

Again, you can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/13_double_spending_02.

Author Details

Phuwanai Thummavet (serial-coder), Lead Blockchain Security Auditor and Consultant | Blockchain Architect and Developer.

See the author’s profile.

About Valix Consulting

Valix Consulting is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise, industry knowledge, and support staff, strive to deliver consistently superior quality services.

For any business inquiries, please contact us via Twitter, Facebook, or info@valix.io.

--

--

Phuwanai Thummavet
Valix Consulting

Blockchain | Coding | Hacking — I’m a full-time bug collector. Most of the time I bring some bugs to life. Visit my website: www.serial-coder.com