Solidity Security By Example #13: Double Spending #2
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 theapplyInterest
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. 👽
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).
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. TheFixedNaiveBank
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.