Revolution Protocol - deepplus's results

A protocol to empower communities to raise funds, fairly distribute governance, and maximize their impact in the world.

General Information

Platform: Code4rena

Start Date: 13/12/2023

Pot Size: $36,500 USDC

Total HM: 18

Participants: 110

Period: 8 days

Judge: 0xTheC0der

Id: 311

League: ETH

Collective

Findings Distribution

Researcher Performance

Rank: 49/110

Findings: 2

Award: $83.85

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

32.7129 USDC - $32.71

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
edited-by-warden
M-05

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/ERC20TokenEmitter.sol#L152-L230

Vulnerability details

Impact

Users can buy NontransferableERC20Token by calling buyToken function directly. At that time, the expected amount of tokens they will receive is determined based on current supply and their paying ether amount. But, due to some transactions(such as settleAuction or another user's buyToken) which is running in front of caller's transaction, they can get less token than they expected.

Proof of Concept

The VRGDAC always exponentially increase the price of tokens if the supply is ahead of schedule. Therefore, if another transaction of buying token is frontrun against a user's buying token transaction, the token price can arise than expected.

For instance, let's assume that ERC20TokenEmitter is initialized with following params:

  • target price: 1 ether
  • decay percent: 10 %
  • per time unit: 10 ether

To avoid complexity, we will assume that the supply of token so far is consistent with the schedule. When alice tries to buy token with 5 ether, expected amount is calculated by getTokenQuoteForEther(5 ether) and the value is about 4.87 ether. However, if Bob's transaction to buy tokens with 10 ether is executed before Alice, the real amount which Alice will receive is about 4.43 ether.

You can check result through following test:

    function testBuyTokenWithoutSlippageCheck() public {
        address alice = makeAddr("Alice");
        address bob = makeAddr("Bob");

        vm.deal(address(alice), 100000 ether);
        vm.deal(address(bob), 100000 ether);

        address[] memory recipients = new address[](1);
        recipients[0] = address(1);
        uint256[] memory bps = new uint256[](1);
        bps[0] = 10_000;

        // expected amount of minting token when alice calls buyToken
        int256 expectedAmount = erc20TokenEmitter.getTokenQuoteForEther(5 ether);

        vm.startPrank(bob);
        // assume that bob calls buy token with 10 ether
        erc20TokenEmitter.buyToken{ value: 10 ether }(
            recipients,
            bps,
            IERC20TokenEmitter.ProtocolRewardAddresses({
                builder: address(0),
                purchaseReferral: address(0),
                deployer: address(0)
            })
        );

        vm.stopPrank();

        vm.startPrank(alice);
        // calculate the amount of tokens which alice will actually receive
        int256 realAmount = erc20TokenEmitter.getTokenQuoteForEther(5 ether);

        vm.stopPrank();

        emit log_string("Expected Amount: ");
        emit log_int(expectedAmount);
        emit log_string("Real Amount: ");
        emit log_int(realAmount);

        assertLt(realAmount, expectedAmount, "Alice should receive less than expected if Bob frontrun buyToken");
    }

Therefore, Alice will get about 0.44 ether less tokens than expected since there is no any checking of slippage in buyToken function.

Tools Used

VS Code

Add slippage checking to buyToken function. This slippage checking should be executed only when the user calls buyToken function directly. In other words, it should not be executed when settleAuction calls buyToken function.

Assessed type

Other

#0 - c4-pre-sort

2023-12-22T16:15:36Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T16:15:47Z

raymondfam marked the issue as duplicate of #26

#2 - c4-pre-sort

2023-12-24T06:00:11Z

raymondfam marked the issue as not a duplicate

#3 - c4-pre-sort

2023-12-24T06:00:15Z

raymondfam marked the issue as primary issue

#4 - rocketman-21

2024-01-04T23:23:23Z

this is intended and a consequence of how the VRGDA functions, when people buy tokens the price goes up if it is ahead of schedule

not ideal UX, but not going to fix for now

#5 - c4-sponsor

2024-01-04T23:23:27Z

rocketman-21 (sponsor) acknowledged

#6 - MarioPoneder

2024-01-06T16:27:33Z

Even though the increasing price is intended, it's state of the art to introduce a slippage parameter to protect users from receiving less than expected. Therefore, maintaining Medium severity seems appropriate.

#7 - c4-judge

2024-01-06T16:27:39Z

MarioPoneder marked the issue as satisfactory

#8 - c4-judge

2024-01-06T16:32:12Z

MarioPoneder marked the issue as selected for report

Findings Information

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
sufficient quality report
duplicate-194

Awards

51.1381 USDC - $51.14

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/ERC20TokenEmitter.sol#L179-L188

Vulnerability details

Impact

The amounts of token for buyers and creators isn't accurately calculated.

Proof of Concept

ERC20TokenEmitter enables anyone to purchase the ERC20 governance token at any time through buyToken function. The buyToken function sends a portion of the paid ETH to protocolRewards. And then calculates the amount of tokens to be distributed to buyers and creators at a certain ratio based on the remaining ETH. The buyToken function calculates the amount of ETH according to the specified ratio and then calls the getTokenQuoteForEther function to calculate the each amount of tokens to be distributed to buyers and creators, respectively.

        //Tokens to emit to creators
        int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0
            ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment)
            : int(0);
        // Tokens to emit to buyers
        int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0);

However, since the buyToken function doesn't accurately update emittedTokenWad state(total amount of emitted token), which is used for calculating the token issuance amount in getTokenQuoteForEther function, the calculated value is different from the amount that should actually be minted.

        //Transfer ETH to treasury and update emitted
        emittedTokenWad += totalTokensForBuyers;
        if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;

As you can see above code, it updates emittedTokenWad state after calculating token amounts for both of buyers and creators. This cause incorrect calculation of minting token. Let me explain with example. let's assume that ERC20TokenEmitter is initialized with following params:

  • creatorrateBps: 2_000
  • entropyRateBps: 5_000
  • target price: 1 ether
  • decay percent: 10 %
  • per time unit: 10 ether

Alice is trying to call buyToken function with over 5 ether by passing her address as addresses param, and 10_000(100%) as basisPointSplits param. Assume that remaining ETH value is 5 ether after sending a portion of msg.value to protocolRewards. Then, the amount of ETH which will be used to mint for buyers is 4 ether: 5 * (10000 - 2000) / 1000 = 4 (ether)

	uint256 toPayTreasury = (msgValueRemaining * (10_000 - creatorRateBps)) / 10_000;

And, the amount of ETH which will be used to mint for creators is 0.5 ether: (5 - 4) * 5000 / 10000 = 0.5 (ether) (5 - 4) - 0.5 = 0.5 (ether)

    //Ether directly sent to creators
    uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000;
    //Tokens to emit to creators
    int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0
        ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment)
        : int(0);

And total amount of minting token is getTokenQuoteForEther(4 ether) + getTokenQuoteForEther(0.5 ether). But buyTokenAmount isn't update the emittedTokenWad between calculating token amount for crators and buyers. This causes wrong calculating price of token based on paid ETH for buyers because VRGDA's calculation is based on total supply and paid ETH amount. As a result, more ERC20 token is minted than expected. You can check result through the following test:

    function testComparingTokenAmountWithUpdatingEmittedAmount() public {
        address alice = makeAddr("Alice");
        vm.deal(address(alice), 100000 ether);

        vm.startPrank(alice);

        address[] memory recipients = new address[](1);
        recipients[0] = address(1);
        uint256[] memory bps = new uint256[](1);
        bps[0] = 10_000;

        // ensure that enough volume was bought for the day, so purchase expectedVolume amount first
        erc20TokenEmitter.buyToken{ value: expectedVolume }(
            recipients,
            bps,
            IERC20TokenEmitter.ProtocolRewardAddresses({
                builder: address(0),
                purchaseReferral: address(0),
                deployer: address(0)
            })
        );

        // first, calculate the actual amount of tokens for buyers and creators.
        int256 for_buyer_1 = erc20TokenEmitter.getTokenQuoteForEther(4 ether);
        int256 for_creators_1 = erc20TokenEmitter.getTokenQuoteForEther(0.5 ether);

        // first, calculate the amount of tokens for buyers and creators with updating emittedTokenWad btw them.
        int256 for_buyer_2 = erc20TokenEmitter.getTokenQuoteForEther(4 ether);
        erc20TokenEmitter.setEmittedTokenWad(erc20TokenEmitter.emittedTokenWad() + for_buyer_2);
        int256 for_creators_2 = erc20TokenEmitter.getTokenQuoteForEther(0.5 ether);

        emit log_string("Current Calc: ");
        emit log_int(for_buyer_1 + for_creators_1);
        emit log_string("Calc with updating emittedTokenWad btw them: ");
        emit log_int(for_buyer_2 + for_creators_2);

        // Assert that the new token amount is less than the previous tokenAmount
        assertGt(for_buyer_1 + for_creators_1, for_buyer_2 + for_creators_2, "");
        vm.stopPrank();
    }

The result is:

[PASS] testComparingTokenAmountWithUpdatingEmittedAmount() (gas: 360782) Logs: Current Calc: 4012115812150412050 Calc with updating emittedTokenWad btw them: 3995502668080304120

As you can see, more ERC20 token is minted.

Tools Used

VSCode

Move line 188 to line 182:

        uint256 creatorDirectPayment = ((msgValueRemaining - toPayTreasury) * entropyRateBps) / 10_000;
        //Tokens to emit to creators
        int totalTokensForCreators = ((msgValueRemaining - toPayTreasury) - creatorDirectPayment) > 0
            ? getTokenQuoteForEther((msgValueRemaining - toPayTreasury) - creatorDirectPayment)
            : int(0);

+	if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;

        // Tokens to emit to buyers
        int totalTokensForBuyers = toPayTreasury > 0 ? getTokenQuoteForEther(toPayTreasury) : int(0);

        //Transfer ETH to treasury and update emitted
        emittedTokenWad += totalTokensForBuyers;
-       if (totalTokensForCreators > 0) emittedTokenWad += totalTokensForCreators;

Assessed type

Other

#0 - c4-pre-sort

2023-12-22T17:32:18Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-12-22T17:32:28Z

raymondfam marked the issue as duplicate of #194

#2 - c4-sponsor

2023-12-27T20:00:33Z

rocketman-21 (sponsor) confirmed

#3 - c4-judge

2024-01-06T14:01:45Z

MarioPoneder marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter