Solidity Security By Example #12: Amplification Attack (Double Spending #1)

Phuwanai Thummavet
Valix Consulting
Published in
8 min readMay 23, 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.

An amplification attack can happen when a smart contract is misdesigned, resulting in the contract being exploited. This article will explain how a smart contract with a design flaw can be attacked and how to deal with the issue. Enjoy reading. 😊

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

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 Dependencies
  • The Vulnerability
  • The Attack
  • The Solutions
  • Summary

The Dependencies

The code below contains dependencies required by the InsecureMoonDAOVote and the FixedMoonDAOVote contracts. The dependencies include ReentrancyGuard abstract contract (lines 3–12), IMoonToken interface (lines 14–23), and MoonToken contract (lines 25–99).

The ReentrancyGuard contains the noReentrant modifier that is used to prevent reentrancy attacks. The noReentrant (lines 6–11) is a simple lock that allows only a single entrance to the function applying it. If there is an attempt to do the reentrant, the transaction would be reverted by the require statement in line 7.

The IMoonToken interface defines function prototypes, enabling the InsecureMoonDAOVote and the FixedMoonDAOVote contracts to interact with the MoonToken contract.

Lastly, the MoonToken contract is a simple ERC-20 token. Users can buy MOON tokens with the corresponding number of Ethers via the buy function (lines 39–46). Users can also sell their MOONs through the sell function (lines 48–55), transfer their MOONs via the transfer function (lines 57–63) and the transferFrom function (lines 65–78), and approve the token transfer to a spender via the approve function (lines 80–82).

Additionally, users can query the transfer approval allowance to a spender by calling the allowance function (lines 84–90), get the total number of Ethers locked in the contract by way of the getEtherBalance function (lines 92–94), and get their balances by consulting the getUserBalance function (lines 96–98).

In addition to the MOON token, it is a non-divisible token with zero token decimals (line 37). Users can buy, sell, or transfer 1, 2, 3, or 46 MOONs but not 33.5 MOONs.

Besides the non-divisible characteristic, the MOON token is also a stablecoin pegged with the Ether (line 30). In other words, 1 MOON will always be worth 1 Ether.

The Vulnerability

The following code exhibits the InsecureMoonDAOVote contract. This contract provides the voting functionality for all MOON token holders to cast a vote for CEO candidates freely — id: 0 for Bob, id: 1 for John, and id: 2 for Eve (lines 28–41).

Within a voting period, a user can cast a vote for only one candidate of their choice by invoking the vote function (lines 44–59), and check for their vote via the getUserVote function (lines 65–67).

Further, users can query the candidate list by consulting the functions getTotalCandidates (lines 61–63) and getCandidate (lines 69–72).

Unquestionably, the InsecureMoonDAOVote contract must be vulnerable. Can you catch up on the issue? 👀

As you can see, the InsecureMoonDAOVote contract is straightforward. The more MOON tokens a user possesses, the more voting points they can give to their candidate (line 58). ✍️

Unfortunately, the InsecureMoonDAOVote contract got a design flaw in the voting mechanism, allowing an attacker to perform a voting amplification attack. 😲

Figure 1 below depicts how an attacker can exploit the InsecureMoonDAOVote contract’s voting mechanism to mastermind the winner.

Figure 1. How an attacker exploits the vulnerable voting mechanism

The upper part of Figure 1 pictures how a regular voter possessing 100 MOONs casts a vote for the candidate Bob. The lower part portrays how an attacker performs an amplification attack on voting for their candidate Eve.

In more detail, the attacker uses their 100 MOONs to vote for Eve (Steps 1 and 2) like a regular voter.

Since the InsecureMoonDAOVote contract only records the vote of each voter and will not allow the same voter to cast a double vote (line 46), the attacker can bypass this condition check by transferring their 100 MOONs to another Sybil account (Step 3). 😎

For this reason, the attacker can double-spend the vote on the spent tokens (Step 4), amplifying Eve’s voting points easily (Step 5). 😈

Let’s demystify the root cause in short. We found that the voting mechanism of the InsecureMoonDAOVote contract lacks locking up the spent MOON tokens after the vote. Please refer to the Solutions section below for the remediation solutions. 🤕

Are there the voting amplification vulnerabilities in real production?

For sure, we discovered the voting amplication vulnerability of the on-chain voting mechanism of the SUSHI token, which can also affect every forked project that adopts that voting functionality to be attacked. 🕵️

Moreover, we also discovered other voting vulnerabilities including voting displacement and redelegation failure. Head to our discovery report for details of our findings. 🕵️

The Attack

The following code presents the contracts AttackServant (lines 9–26) and AttackBoss (lines 28–59). Both contracts facilitate an attacker to take control of the voting winner easily.

To exploit the InsecureMoonDAOVote contract’s voting mechanism, an attacker performs the following actions:

  1. Deploy the attackBoss contract (lines 32–35) by passing addresses of the MoonToken contract and the InsecureMoonDAOVote contract as deployment arguments.
  2. Trigger the attack by executing the attackBoss.attack() function (lines 38–58) and inputting as function arguments the target candidate’s id (i.e., _candidateID) and the number of times they would like to amplify the vote on the target candidate (i.e., _xTimes).

The following describes how the attackBoss.attack() function would perform the exploitation under the hood. 🥷

  1. The attackBoss.attack() function transfers all attacker’s MOON tokens to the contract itself (line 43).
  2. The attackBoss.attack() function executes the loop for orchestrating multiple servant contract instances (lines 45–54) as per the _xTimes parameter (line 45).

    2.1 — In each iteration, the attackBoss.attack() function deploys a single servant contract instance from the AttackServant contract (line 47).

    2.2 — The attackBoss.attack() function transfers all MOON tokens to the previously deployed servant instance (line 50).

    2.3 — The attackBoss.attack() function invokes the servant.attack(_candidateID) function (line 53) to cast a vote for the given _candidateID.

    2.4 — The servant.attack() function performs the vote for the target candidate (line 23) and then transfers all MOON tokens back to the attackBoss mother instance (line 24).

    2.5 — The attackBoss.attack() function proceeds with the new iteration until it fulfills the _xTimes parameter (line 45).
  3. Finally, the attackBoss.attack() function transfers all MOON tokens back to the attacker (line 57) and finishes the attack execution.
Figure 2. How the AttackBoss mother contract instance orchestrates multiple AttackServant instances to take over the voting winner

For the sake of illustration, consider Figure 2 above as an example. The attacker initiates the attack by triggering the AttackBoss mother contract instance and approving 100 MOONs as initial tokens for the attack (Step 1).

The AttackBoss mother instance sequentially deploys three AttackServant child instances to amplify the voting points for the candidate Eve (Steps 2–10). 🧙‍♂️

Eventually, the AttackBoss instance transfers 100 MOONs back to the attacker (Step 11). As a result of the attack, the voting points for Eve have been gained by 300 points — i.e., 3X amplification (Step 12). 🧞‍♂️

Figure 3. The attack result

Figure 3 above displays our attack simulation result. As you can see, the attacker could gain 200 voting points for Eve by using only 10 MOONs (i.e., 20X voting amplification) in only a single attack transaction. 🤯

The Solutions

There are two preventive solutions to remediate the voting amplification vulnerability. 👨‍🔧

  1. Locking up the spent MOON tokens after the vote
  2. Applying the delegation and checkpoint approach to keep track of voting points at each specific block number

In this article, we will demonstrate the first solution (Locking up the spent MOON tokens after the vote). 👍

For the second solution (Applying the delegation and checkpoint approach to keep track of voting points), it requires both the MoonToken and the InsecureMoonDAOVote contracts to be completely redesigned.

Besides, the new design also requires users a completely different approach to interacting with the contracts. For this reason, the second solution will not be presented in this article. 🤡

In case you might be interested, nonetheless, please refer to the ERC20Votes implementation by OpenZeppelin.

This section will describe the first solution (Locking up the spent MOON tokens after the vote). Let’s take a look at the FixedMoonDAOVote contract above.

The idea is to lock away all MOONs already used for voting for a certain period. Later on, voters can withdraw their MOONs after the voting period ends. This way, we can guarantee that no one can double-spend their MOONs. 👌

To achieve this, we introduced a new UserVote struct property named moonWithdrawn (line 14) for tracking the MOON token’s withdrawal status.

The moonWithdrawn variable will be false once a voter executes the vote function (line 61). The variable will be true when a voter withdraws their MOONs (line 74).

To lock up the MOON tokens, the vote function will transfer all MOONs from a voter account to the FixedMoonDAOVote contract itself (line 55). 🤔

For voters to withdraw their MOONs, we implemented the withdrawMoonTokens function (lines 68–78). This function suddenly allows voters to retrieve their MOONs after the voting period. 🤝

Summary

In this article, you have learned how a design flaw can lead to an amplification attack in the smart contract. You have understood how an attacker exploits the vulnerable contract and how to tackle the issue.

We hope this article could gain your security knowledge. See you again in our next article.

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

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