xanoutas/solidity-security-findings huggingface dataset:
Logic error on _deployERC20 can lead to DoS of legitimate mint and buyCurvesTokenWithName transactions
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L343-L348


# Vulnerability details

## Impact
Misusing or maliciously intentionally using the function `buyCurvesTokenWithName`  can block the mint function and partially block the buyCurvesTokenWithName function. 

`buyCurvesTokenWithName` calls `_mint`, that calls  `_deployERC20`. It will check if the symbol of the token to be created is equal to CURVES and will concatenate an integer value to the name to not create tokens with the same symbol if that is the case.

But we can call `buyCurvesTokenWithName` with `CURVES+[_curvesTokenCounter + 1]` as an argument. Consequently, `curvesTokenCounter` will not be incremented, making it impossible for other users to create a token with the CURVES symbol.


## Proof of Concept
Here is the PoC code for this case. 

Save it to a file named `dos-token-symbol.ts` and save it to `/tests`. Then run `npx hardhat test`. 

Note that it have the `it.only()` part to run only this test. If you want to run it with the other tests of your test suite just remove the `only` keyword.   

We can see in the PoC code that both legimitate `mint` and `buyCurvesTokenWithName` transactions will fail.

```typescript
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 { deployCurveContracts } from "./test.helpers";

describe("DoS vulnerability PoC", () => {
  let testContract: Curves, owner: SignerWithAddress, friend: SignerWithAddress;

  describe("buyCurvesTokenWithName test", () => {
    it.only("Should be able to mint with name and withdraw", async () => {
        // setup
        [owner, friend] = await ethers.getSigners();
        testContract = await deployCurveContracts();

        // deploying the token with malicious name
        await testContract.connect(friend).buyCurvesTokenWithName(friend.address, 1, "BadToken", "CURVES1");
        
        // now the legitimate buy will fail
        let legit_buy = testContract.buyCurvesTokenWithName(owner.address, 1, "LegitToken", "CURVES");
        expect(legit_buy).to.be.revertedWith("InvalidERC20Metadata()");
        
        // and the mint will fail as well
        let legit_mint = testContract.mint(owner.address)
        expect(legit_mint).to.be.revertedWith("InvalidERC20Metadata()");    
    });
  });
});
```


## Tools Used
hardhat

## Recommended Mitigation Steps
Change the logic on `_deployERC20` to increment +1 to `_curvesTokenCounter` until finding a symbol that is not in use.


## Assessed type

DoS
Lost fees will be frozen in the FeeSplitter contract forever
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L73-L78


# Vulnerability details

Due to the `getClaimableFees` function, the balance is a crucial component in this calculation. If a token holder withdraws tokens, the balance changes, and claimable fees will be lost:
``` solidity
function getClaimableFees(address token, address account) public view returns (uint256) {
    TokenData storage data = tokensData[token];
    uint256 balance = balanceOf(token, account);
    uint256 owed = (data.cumulativeFeePerToken - data.userFeeOffset[account]) * balance;
    return (owed / PRECISION) + data.unclaimedFees[account];
}
```
However, the `FeeSplitter` contract does not distribute lost tokens among other holders, and it lacks any function for the protocol owner to withdraw these lost tokens.

## Impact
* Lost fess will be frozen in the contract forever.

## Proof of Concept
``` solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;

import "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {CurvesERC20} from "@contracts/CurvesERC20.sol";
import {CurvesERC20Factory} from "@contracts/CurvesERC20Factory.sol";
import {FeeSplitter} from "@contracts/FeeSplitter.sol";
import {Curves} from "@contracts/Curves.sol";\

contract CurvesPOC is Test {
    CurvesERC20 public curvesERC20;
    CurvesERC20Factory public curvesERC20Factory;
    Curves public curves;
    FeeSplitter public feeSplitter;

    // Addresses
    address public owner = makeAddr("OWNER");
    address public protocolFeeDestination = makeAddr("PROTOCOL_FEE_DESTINATION");
    address public attacker = makeAddr("ATTACKER");
    address public curvesSubject = makeAddr("CURVES_SUBJECT");
    address public alice = makeAddr("ALICE");
    address public bob = makeAddr("BOB");

    function test_lostFeesFreezeInContract() public {
        // Funding users
        vm.deal(alice, 100 ether);

        console.log("Contract balance at start: ", address(feeSplitter).balance);

        // Curve token subject create token
        vm.prank(curvesSubject);
        curves.buyCurvesTokenWithName(curvesSubject, 1, "CURVES_SUBJECT", "CURVES_SUBJECT_SYMBOL");

        // Alice buy curve subject token
        uint256 alice_amount = 1;
        uint256 price = curves.getPrice(curves.curvesTokenSupply(curvesSubject), alice_amount);
        (, , , , uint256 totalFee) = curves.getFees(price);
        uint256 alice_cost = price + totalFee;
        vm.prank(alice);
        curves.buyCurvesToken{value: alice_cost}(curvesSubject, alice_amount);

        uint256 aliceBeforeFees = feeSplitter.getClaimableFees(curvesSubject, alice);
        uint256 subjectBeforeFees = feeSplitter.getClaimableFees(curvesSubject, curvesSubject);
        uint256 depositedFeesBefore = address(feeSplitter).balance;
        console.log("+++ BEFORE ALICE BUY STATE +++");
        console.log("ALICE claimable fees: ", aliceBeforeFees);
Holders will lose unclaimed fees after buying or selling tokens
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L96-L100


# Vulnerability details

Fees are transferred during `_buyCurvesToken` and `sellCurvesToken`, and `onBalanceChange` function called within the `_transferFees`:
``` solidity
// Code snippet ...
if (feesEconomics.holdersFeePercent > 0 && address(feeRedistributor) != address(0)) {
    feeRedistributor.onBalanceChange(curvesTokenSubject, msg.sender);
    feeRedistributor.addFees{value: holderFee}(curvesTokenSubject);
}
// Code snippet ...
```

Due to this, in `onBalanceChange`, unclaimed yield is eliminated by updating `userFeeOffset` to the latest `cumulativeFeePerToken`:
``` solidity
function onBalanceChange(address token, address account) public onlyManager {
    TokenData storage data = tokensData[token];
    data.userFeeOffset[account] = data.cumulativeFeePerToken;
    if (balanceOf(token, account) > 0) userTokens[account].push(token);
}
```

## Impact
* Holders will lose 100% of unclaimed fees, and these fees will be frozen or stuck in the contract forever.

## Proof of Concept
``` solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;

import "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {CurvesERC20} from "@contracts/CurvesERC20.sol";
import {CurvesERC20Factory} from "@contracts/CurvesERC20Factory.sol";
import {FeeSplitter} from "@contracts/FeeSplitter.sol";
import {Curves} from "@contracts/Curves.sol";

contract CurvesPOC is Test {
    CurvesERC20 public curvesERC20;
    CurvesERC20Factory public curvesERC20Factory;
    Curves public curves;
    FeeSplitter public feeSplitter;

    // Addresses
    address public owner = makeAddr("OWNER");
    address public protocolFeeDestination = makeAddr("PROTOCOL_FEE_DESTINATION");
    address public attacker = makeAddr("ATTACKER");
    address public curvesSubject = makeAddr("CURVES_SUBJECT");
    address public alice = makeAddr("ALICE");
    address public bob = makeAddr("BOB");

    function test_holderLostFeesAfterBuyOrSell() public {
        // Funding users
        vm.deal(alice, 100 ether);

        console.log("Contract balance at start: ", address(feeSplitter).balance);

        // Curve token subject create token
        vm.prank(curvesSubject);
        curves.buyCurvesTokenWithName(curvesSubject, 1, "CURVES_SUBJECT", "CURVES_SUBJECT_SYMBOL");

        // Alice buy curve subject token
        uint256 alice_amount = 1;
        uint256 price = curves.getPrice(curves.curvesTokenSupply(curvesSubject), alice_amount);
        (, , , , uint256 totalFee) = curves.getFees(price);
        uint256 alice_cost = price + totalFee;
        vm.prank(alice);
        curves.buyCurvesToken{value: alice_cost}(curvesSubject, alice_amount);

        uint256 aliceBeforeFees = feeSplitter.getClaimableFees(curvesSubject, alice);
        uint256 subjectBeforeFees = feeSplitter.getClaimableFees(curvesSubject, curvesSubject);
Unrestricted Token Purchase During Presale in Curves Smart Contract
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L211-L216


# Vulnerability details

## Impact
This issue allows users to bypass presale-specific restrictions, enabling the purchase of tokens without adhering to whitelisting and maximum buy amount constraints. Specifically, during the presale period, the `buyCurvesToken` function does not enforce the presale rules set in the `buyCurvesTokenWhitelisted` function. Consequently, users can buy tokens irrespective of whether they are on the whitelist and can exceed the maximum buy amount specified for the presale (`presalesMeta[curvesTokenSubject].maxBuy`). This undermines the fairness and integrity of the presale process, potentially leading to an uneven distribution of tokens and dissatisfaction among intended participants. The severity of this issue is quite crucial, given its potential to significantly impact the presale's outcome and the trust of stakeholders in the contract's enforcement of rules.

## Proof of Concept
The vulnerability is present in the `buyCurvesToken` function. The function’s condition (`startTime != 0 && startTime >= block.timestamp`) checks if the presale has started but does not restrict purchases based on the presale rules. This absence of checks allows any user to purchase tokens during the presale period, bypassing the whitelisting and maximum buy amount restrictions. Apparently, if the check isn't reverting, i.e. `startTime != 0 && startTime < block.timestamp`, `_buyCurvesToken(curvesTokenSubject, amount)` will simply be executed unrestricted:

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L211-L216

```solidity
    function buyCurvesToken(address curvesTokenSubject, uint256 amount) public payable {
        uint256 startTime = presalesMeta[curvesTokenSubject].startTime;
        if (startTime != 0 && startTime >= block.timestamp) revert SaleNotOpen();

        _buyCurvesToken(curvesTokenSubject, amount);
    }
```
In contrast, the `buyCurvesTokenWhitelisted` function properly restricts purchases during the presale:

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L404-L420

```solidity
    function buyCurvesTokenWhitelisted(
        address curvesTokenSubject,
        uint256 amount,
        bytes32[] memory proof
    ) public payable {
        if (
            presalesMeta[curvesTokenSubject].startTime == 0 ||
            presalesMeta[curvesTokenSubject].startTime <= block.timestamp
        ) revert PresaleUnavailable();

        presalesBuys[curvesTokenSubject][msg.sender] += amount;
        uint256 tokenBought = presalesBuys[curvesTokenSubject][msg.sender];
        if (tokenBought > presalesMeta[curvesTokenSubject].maxBuy) revert ExceededMaxBuyAmount();

        verifyMerkle(curvesTokenSubject, msg.sender, proof);
        _buyCurvesToken(curvesTokenSubject, amount);
    }
```
## Tools Used
Manual

## Recommended Mitigation Steps
Given that `buyCurvesToken` function i
Curves contains no way to recover stuck Ether`
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L246-L250


# Vulnerability details

## Impact

Ether can get stuck in Curves by buying tokens and sending excess Ether or the `FeeSplitter` being set to the zero address. In these situations funds are stuck inside the contract and theres no way to recover them.

## Proof of Concept

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L246-L250

```solidity
if (feesEconomics.holdersFeePercent > 0 && address(feeRedistributor) != address(0)) {
                feeRedistributor.onBalanceChange(curvesTokenSubject, msg.sender);
                feeRedistributor.addFees{value: holderFee}(curvesTokenSubject);
            }
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

Either return excess Ether to the user and return excess funds if no `FeeSplitter` is set or add a rescue mechanism for trusted accounts.


## Assessed type

ETH-Transfer
Attacker can make default creation of ERC20 to always revert
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L343-L350


# Vulnerability details

## Impact
Attacker can make default creation of ERC20 to always revert. Thus, rendering `mint` and `withdraw` function unusable.

## Bug
Symbol can be set by the attacker such that it will collide with the same symbol as the one which will be created via default values.

## Proof of Concept
Here is the foundry [test file](https://gist.github.com/0x0bserve/3cc104fbc7f0f2969a36d955a4a90ea5)

Here is the test with main logic of the exploit:
```javascript
    function testDefaultDeployERC20Tokens() public {
        // Attacker creates ERC20 token with symbol `CURVES1`
        vm.prank(attacker);
        curves.buyCurvesTokenWithName(attacker, 1, "Curves 1", "CURVES1");

        // Any user tries to create ERC20 token with default name and symbol using `mint` function
        vm.prank(user);
        vm.expectRevert();
        curves.mint(user);

        // Creator cannot use `withdraw` function without deploying it's
        // ERC20 token via `buyCurvesTokenWithName` or `setNameAndSymbol` functions
        vm.startPrank(creator);
        curves.buyCurvesToken(creator, 1);
        curves.buyCurvesToken{value: 1 ether}(creator, 1);
        vm.expectRevert();
        curves.withdraw(creator, 1);
        vm.stopPrank();

        // Any user cannot use `withdraw` function without creator deploying it's
        // ERC20 token via `buyCurvesTokenWithName` or `setNameAndSymbol` functions
        vm.startPrank(user);
        curves.buyCurvesToken{value: 1 ether}(creator, 1);
        vm.expectRevert();
        curves.withdraw(creator, 1);
        vm.stopPrank();
    }
```
Here are the step of exploit:
1. Attacker creates ERC20 token with symbol `CURVES1` using `buyCurvesTokenWithName` function.
2. This symbol name will collide the symbol which will be created via default values.
3. This will result in revert every time `mint` function is called and `withdraw` function is called when there is no ERC20 token.

## Tools Used
Foundry

## Recommended Mitigation Steps
Add these lines of code in `setNameAndSymbol` and `buyCurvesTokenWithName` functions of `Curves` contract such that attacker cannot make the same default symbol. 
```javascript
    function buyCurvesTokenWithName(
        address curvesTokenSubject,
        uint256 amount,
        string memory name,
        string memory symbol
    ) public payable {
        uint256 supply = curvesTokenSupply[curvesTokenSubject];
        if (supply != 0) revert CurveAlreadyExists();

+        symbol = string(abi.encodePacked(symbol, "_CURVES"));
+        _buyCurvesToken(curvesTokenSubject, amount);
+        _mint(curvesTokenSubject, name, symbol);
    }
```
```javascript
    function setNameAndSymbol(
        address curvesTokenSubject,
        string memory name,
        string memory symbol
    ) external onlyTokenSubject(curvesTokenSubject) {
        i
Use of transfer can cause DOS
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/FeeSplitter.sol#L85
https://github.com/code-423n4/2024-01-curves/blob/main/contracts/FeeSplitter.sol#L116


# Vulnerability details

## Impact
Usage of deprecated transfer can revert and cause DOS.

## Proof of Concept
The transfer function is used to send eth using a fixed stipend 2300 gas. This was built to prevent reentrancy. However, this limits your protocol to interact with other contracts and cause DOS during claiming of fees.

A good article about that: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Used call instead. For example

```solidity
(bool success, ) = payable(msg.sender).call{claimable}("");
require(success, "Transfer failed.");
```


## Assessed type

ETH-Transfer
Token cannot be sold if all other tokens are withdrawn
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L91


# Vulnerability details



## Impact
A token cannot be sold, even when it is not the last token, if all other tokens are withdrawn.

## Proof of Concept
If all but one token are withdrawn then `FeeSplitter.totalSupply(token) == 1 * PRECISION`. [This is calculated as `(curves.curvesTokenSupply(token) - curves.curvesTokenBalance(token, address(curves))) * PRECISION`](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L45).
When a token is sold (`sellCurvesToken()`) [`curvesTokenSupply[token]` is reduced](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L290), which in this case results in `FeeSplitter.totalSupply(token) == 0`, before transferring fees.
Fees are transferred by [`FeeSplitter.addFees()`](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L89-L94) but this
```solidity
uint256 totalSupply_ = totalSupply(token);
if (totalSupply_ == 0) revert NoTokenHolders();
```
, i.e. reverts in this case.

Thus the seller is prevented from selling his token. This of course generalizes to not being able to sell several tokens at once if all other tokens are withdrawn.

## Recommended Mitigation Steps
Handle the holder fee correctly by excluding it from the total fee, or sending it to the protocol, in the case there are no holders left.


## Assessed type

Other
Only the most recently added holder fees can be claimed
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L98


# Vulnerability details



## Impact
Only the most recently added holder fees can be claimed, the previous are overwritten and lost.

## Proof of Concept
```solidity
function onBalanceChange(address token, address account) public onlyManager {
    TokenData storage data = tokensData[token];
    data.userFeeOffset[account] = data.cumulativeFeePerToken;
    if (balanceOf(token, account) > 0) userTokens[account].push(token);
}
```
zeroes the offset, which means that the accrued fees implied by the previous difference are zeroed out. The only remaining account of the fees is `data.unclaimedFees[account]`, but this is only increased in `updateFeeCredit()`, which is only called when claiming fees (`claimFees()` or `batchClaiming()`).

`onBalanceChange()` is only called (in `Curves._transferFees()`) when tokens are bought or sold, immediately before holder fees are added (by `addFees()`) (which is the only instance where holder fees are added).

Thus whenever holder fees are added, then previously added but hitherto unclaimed holder fees are lost and overwritten.

## Recommended Mitigation Steps
Make sure to account for previous holder fees by adding them to `data.unclaimedFees[account]` before zeroing the offset, i.e. a full `updateFeeCredit()`.


## Assessed type

Other
`  FeeSplitter :: claimFees ` may cause some external rewards to be locked in the contract
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L80


# Vulnerability details

## Impact
`claimFees` will update rewardToken.lastBalance so that if there are unaccrued reward tokens in the contract, users will not be able to claim them.

 
## Tools Used
manual 

## Recommended Mitigation Steps

calculate mint tokens and burns must be equal to the total supply
`claimFees` to accrue rewards.


## Assessed type

Error
Potential Loss of Fees Due to Token Transfer Prior to Claim in `FeeSplitter.sol`
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/FeeSplitter.sol#L80


# Vulnerability details

### Impact
Users of the `FeeSplitter` contract may lose their accumulated fees if they transfer their tokens before claiming. The fee distribution mechanism calculates fees per user at the moment of claiming based on the global fee pool for each token. If a user transfers their tokens before claiming, they may not receive fees accrued during their holding period.

---

### Proof of Concept
In `FeeSplitter.sol`, the `claimFees` function computes the fees a user can claim based on their current token balance. The fee calculation does not account for changes in token balance over time, leading to a potential loss of fees for users who transfer their tokens before claiming.

#### Code Snippet:
```solidity
function claimFees(address token) external {
    // @audit-issue H-02 Risk of losing fees due to token transfer before claiming
    updateFeeCredit(token, msg.sender);
    uint256 claimable = getClaimableFees(token, msg.sender);
    if (claimable == 0) revert NoFeesToClaim();
    tokensData[token].unclaimedFees[msg.sender] = 0;
    payable(msg.sender).transfer(claimable);
    emit FeesClaimed(token, msg.sender, claimable);
}
```

```solidity
// In Curves.t.sol
function testUserCanLoseYield() public {
        deal(alice, 1 ether);
        deal(bob, 10 ether);
        vm.prank(alice);
        instance.buyCurvesToken(alice, 1);

        vm.startPrank(bob);
        uint256 initialBalance = bob.balance;
        uint256 sharesToBuy = 3;
        instance.buyCurvesToken{value: 3 ether}(alice, sharesToBuy);
        vm.stopPrank();

        assertNotEq(splitter.getClaimableFees(alice, bob), 0);

        vm.startPrank(bob);
        instance.transferCurvesToken(alice, jane, sharesToBuy);
        vm.expectRevert();
        splitter.claimFees(alice);
        vm.stopPrank();
    }
```

### Tools Used
Manual Review

### Recommended Mitigation Steps
To prevent users from losing their fees, the contract should implement a mechanism that accounts for token holdings over time when calculating fees. This could involve:

- Tracking Token Holding Durations: Implement a system to track how long each user holds their tokens.
- Proportional Fee Distribution: Adjust the fee distribution logic to allocate fees based on the duration of token holding, ensuring users receive fees proportional to the time they held the tokens.



## Assessed type

Error