xanoutas/solidity-security-findings huggingface dataset:
LPholders can be prevented from burning their position NFTs
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/periphery/NonfungiblePositionManager.sol#L197
https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/periphery/NonfungiblePositionManager.sol#L398


# Vulnerability details

### Proof of Concept

NonfungiblePositionManager.sol's burn function requires that the position to burn has not liquidity or tokensOwed left.

```solidity
    function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) {
        Position storage position = _positions[tokenId];
>>>     if (position.liquidity > 0 || position.tokensOwed0 > 0 || position.tokensOwed1 > 0) revert NotCleared();
        delete _positions[tokenId];
        _burn(tokenId);
    }
```

But `increaseLiquidity` has no access control, and anyone can increase a position's liquidity by any amount, no matter how little.

```solidity
    function increaseLiquidity(
        IncreaseLiquidityParams calldata params
    )
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint128 liquidity, uint256 amount0, uint256 amount1)
    {
        Position storage position = _positions[params.tokenId];

        PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];

        IRamsesV3Pool pool;
        (liquidity, amount0, amount1, pool) = addLiquidity(
            AddLiquidityParams({
                token0: poolKey.token0,
                token1: poolKey.token1,
                tickSpacing: poolKey.tickSpacing,
                tickLower: position.tickLower,
                tickUpper: position.tickUpper,
                amount0Desired: params.amount0Desired,
                amount1Desired: params.amount1Desired,
                amount0Min: params.amount0Min,
                amount1Min: params.amount1Min,
                recipient: address(this),
                index: params.tokenId
            })
        );

// ...
        unchecked {
            position.liquidity += liquidity;
        }

```   

A malicious user in a griefing attack can continously increase liquduity by as low as 1 wei to continously dos attempt to burn a position.


### Recommended Mitigation Steps

Ways to fix this include
1. Automatically removing liquidity when `burn` is called;
2. Introducing a minimum amount of liquidity that can be added by a non-owner to a position. This will make the attack more expensive and disincentivize it.
3. Ensuring only a position owner can increase his liquidity.


## Assessed type

Access Control
getReward should not forcefully transfer reward to msg.sender
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/gauge/GaugeV3.sol#L431


# Vulnerability details

## Proof of Concept

NFT owner Or nft operator can [getReward from gauge contract](https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/gauge/GaugeV3.sol#L431)

```solidity
    function getReward(uint256 tokenId, address[] memory tokens) public lock {
        INonfungiblePositionManager _nfpManager = nfpManager;
        address owner = _nfpManager.ownerOf(tokenId);
        address operator = _nfpManager.getApproved(tokenId);
        /// @dev check if owner, operator, or approved for all
        require(
            msg.sender == owner ||
                msg.sender == operator ||
                _nfpManager.isApprovedForAll(owner, msg.sender),
            "Not authorized"
        );

        (, , , int24 tickLower, int24 tickUpper, , , , , ) = _nfpManager
            .positions(tokenId);

        _getAllRewards(
            address(_nfpManager),
            tokenId,
            tickLower,
            tickUpper,
            tokens,
            msg.sender // <=== FOUND
        );
    }
```

if an approved token spender can claim all reward for owner, but the rewards are forcerfully transferred to msg.sender.

While the approved token spender can help owner claim reward, it is may not expect that the address receive the reward,

Otherwise, this cause loss of reward for nft owner.

## Recommended Mitigation Steps

```solidity
    function getReward(uint256 tokenId, address[] memory tokens, address receiver) public lock {
        INonfungiblePositionManager _nfpManager = nfpManager;
        address owner = _nfpManager.ownerOf(tokenId);
        address operator = _nfpManager.getApproved(tokenId);
        /// @dev check if owner, operator, or approved for all
        require(
            msg.sender == owner ||
                msg.sender == operator ||
                _nfpManager.isApprovedForAll(owner, msg.sender),
            "Not authorized"
        );

        (, , , int24 tickLower, int24 tickUpper, , , , , ) = _nfpManager
            .positions(tokenId);

        _getAllRewards(
            address(_nfpManager),
            tokenId,
            tickLower,
            tickUpper,
            tokens,
            receiver // <=== FIX
        );
    }
```




## Assessed type

Access Control
Only 1/8 of the fee is captured for feeCollector rather than the actual % such as 80%
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/core/RamsesV3Pool.sol#L713
https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/core/RamsesV3Pool.sol#L718


# Vulnerability details

## Proof of Concept
A percentage of fees for a pool are set aside for the `feeCollector`. These collected fees are then distributed to voters and the treasury pro-rata. The percentage of fees is set in the `RamsesV3Factory` contract and can be used by any pool deployed as can be seen in the Slot0 struct for a pool below for example:

```solidity
struct Slot0 {
    ...
 @> uint8 feeProtocol;
    ...
}
```

When the factory contract is deployed, the `feeProtocol` is initially set to 80 (specifying 80% of fees are to be paid to the `feeCollector` address)

```solidity
constructor(address accessManager) AccessManaged(accessManager) {
        ...

        /// @dev the initial feeSplit of what is sent to the FeeCollector to be distributed to voters and the treasury
        /// @dev 80% to FeeCollector
 @>     feeProtocol = 80;

        ...
    }
```

But when we look at the `flash` function below for a pool for example, the calculation for capturing the % of the fees that go to the protocol is wrong because it's calculating for 1/n % rather than n%

```solidity
function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external override lock {
        ...

        unchecked {
            ...
            if (paid0 > 0) {
@>                uint256 pFees0 = feeProtocol == 0 ? 0 : paid0 / feeProtocol;
                if (uint128(pFees0) > 0) $.protocolFees.token0 += uint128(pFees0);
                $.feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - pFees0, FixedPoint128.Q128, _liquidity);
            }
            if (paid1 > 0) {
@>                uint256 pFees1 = feeProtocol == 0 ? 0 : paid1 / feeProtocol;
                if (uint128(pFees1) > 0) $.protocolFees.token1 += uint128(pFees1);
                $.feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - pFees1, FixedPoint128.Q128, _liquidity);
            }

            ...
        }
    }
```

A small POC in foundry is available below:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";

contract TestCastAddyZero is Test {

  function setUp() public {

  }

  function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            // Compute the product mod 2**256 and mod 2**256 - 1
            // then use the Chinese Remainder Theorem to reconstruct
            // the 512 bit result. The result is stored in two 256
            // variables such that product = prod1 * 2**256 + prod0
            uint256 prod0; // Least significant 256 bits of the product
            uint256 prod1; // Most significant 256 bits of the product
            assembly {
Imprecise tick cumulative is calculated inside the Oracle observations
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/core/libraries/Oracle.sol#L284-L286


# Vulnerability details

## Proof of Concept
Solidity rounds down the result of an integer division, and because of that, it is always recommended to multiply before dividing to avoid that precision loss. In the case of a prior division over multiplication, the final result may face serious precision loss as the first answer would face truncated precision and then multiplied to another integer.
The problem arises in Oracle's observation part when it tries to calculate the tick cumulative. After checking the timestamps, the next step is to calculate the tick cumulative and seconds per liquidity cumulative.
If we look deeply at the function `observeSingle()` we can see the tick cumulative calculation is presented as:

```Solidity
    function observeSingle(
        Observation[65535] storage self,
        uint32 time,
        uint32 secondsAgo,
        int24 tick,
        uint16 index,
        uint128 liquidity,
        uint16 cardinality
    ) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) {

    ...

            } else {
                /// @dev we're in the middle
                uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp;
                uint32 targetDelta = target - beforeOrAt.blockTimestamp;
                return (
                    beforeOrAt.tickCumulative +
                        ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / int56(uint56(observationTimeDelta))) *
                        int56(uint56(targetDelta)),
                    beforeOrAt.secondsPerLiquidityCumulativeX128 +
                        uint160(
                            (uint256(
                                atOrAfter.secondsPerLiquidityCumulativeX128 -
                                    beforeOrAt.secondsPerLiquidityCumulativeX128
                            ) * targetDelta) / observationTimeDelta
                        )
                );
    }
```

https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/core/libraries/Oracle.sol#L284-L286

If the target is between the `beforeOrAt` or `atOrAfter`, the `tickCumulative` will be computed as:

$$ tickCumulative = tickCumulative_{before} + ( \frac{tickCumulative_{after} - tickCumulative_{before}}{observationTime} \times targetDelta ) $$

The problem here is that, in some cases, the tick cumulative would be calculated wrongly, as this pattern prioritizes division over multiplication. This model also lacks sufficient sensitivity for division accuracy.

You can run this code to see the difference between the results:

```Solidity
    function test_precissionLoss() public {

        uint beforeOrAtTickCumulative = 212814;
        uint atOrAfterTickCumulative = 617253;
        uint observationTimeDelta = 562310;
        uint targetDelta = 524790;

        uint tickCumulative_a
Stuck rewards in GaugeV3 due to improper timing coordination and period dependency on `block.timestamp`
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/gauge/GaugeV3.sol#L321-L325


# Vulnerability details

## Proof of Concept
The function `cachePeriodEarned()` inside the GaugeV3 contract, is responsible for showing how much an LP has earned reward. This function calculates the earned amount of a LP position by computing the position's periods in seconds which is in the target range of upper and lower ticks that makes the position eligible, as well as the total supply of the reward token which is injected via the `notifyRewardAmounts()` function. The mentioned earning amount is then computed as:

$$ amount = \frac{totalSupply \times periodsInSecond_{X96}}{week_{X96}} $$

The point here is that, the contract's timing system depends on the present time (now or 'block.timestamp`) rather than the pool creation time. In other words, if the LPs start to add liquidity and hence, the LTs start to trade after one period (week), but the admin calls `notifyRewardAmounts()` in the present period, those rewards would get stuck inside the contract and couldn't be released:

```Solidity

    function cachePeriodEarned(
        uint256 period,
        address token,
        address owner,
        uint256 index,
        int24 tickLower,
        int24 tickUpper,
        bool caching
    ) public override returns (uint256 amount) {
        uint256 periodSecondsInsideX96;

    ...

        amount = FullMath.mulDiv(
            tokenTotalSupplyByPeriod[period][token],
            periodSecondsInsideX96,
            WEEK << 96
        );

    ...
    }
```
https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/gauge/GaugeV3.sol#L321-L325

The problem here is that the total supply of the reward is not a single storage (e.g. a `uint256`), but instead a mapping. The `tokenTotalSupplyByPeriod[period][token]` is a mapping to keep record of the rewards added by a period, and thus, if a period lacks of any liquidity inside the pool, its value wouldn't be used anymore and leads to the reward token loss of the admin or any entity responsible for calling the `notifyRewardAmounts()`. Inside the `cachePeriodEarned()` function it does not accumulate the non-distributed rewards from the past.

This scenario shows the impact of this issue:

1. The pool is created on Day 5 of the week, leaving 30 hours before the week ends and the new period begins.No liquidity has been added yet.
2. On Day 6, the `notifyRewardAmount()` function is called, adding 1 ether worth of reward tokens to the pool for distribution. The reward is set to be distributed during the current period.
3. The next day, on Day 7 (which marks the start of a new week/period), LPs begin to add liquidity and Liquidity Takers start to trade. The period has shifted due to the transition to the new week.
`tokenTotalSupplyByPeriod` is Reset to 0 for New Period.
4. Since the new period begins, `tokenTotalSupplyByPeriod` is reset to 0 for the new week. This create
Oracle Miscalculation During Zero Liquidity Periods Leads to Inaccurate AMM Data
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/core/libraries/Oracle.sol#L24


# Vulnerability details

## Description

The transform function is a core component of the Oracle system. It's used to update observations of the pool state over time, which are critical for accurate price feeds, liquidity calculations, and potentially reward distributions.

The issue, however, lies in the handling of the liquidity parameter when it's zero. The current implementation uses a ternary operator to avoid division by zero, but this approach can lead to inaccurate accumulation of secondsPerLiquidityCumulativeX128 when the pool has no liquidity.

## Code
```solidity
function transform(
    Observation memory last,
    uint32 blockTimestamp,
    int24 tick,
    uint128 liquidity
) private pure returns (Observation memory) {
    unchecked {
        uint32 delta = blockTimestamp - last.blockTimestamp;
        return Observation({
            blockTimestamp: blockTimestamp,
            tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)),
            secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +
                ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),
            initialized: true
        });
    }
}
```

The issue is in this line:

```solidity
((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1))
```

When liquidity is zero, we're dividing by 1 instead of zero to avoid a division-by-zero error. While this prevents the function from reverting, it introduces a logical error in the calculations.

## Impact

- Inaccurate Liquidity Tracking: During periods of zero liquidity, the function is still accumulating time as if there was 1 unit of liquidity. This leads to an overestimation of the time-weighted average liquidity.

- Misleading Oracle Data: Any system relying on this oracle for liquidity data will receive inaccurate information. This could affect trading decisions, risk assessments, or any other mechanisms that depend on accurate liquidity reporting.


## Scenario

Imagine a pool that alternates between having liquidity and being empty:

- T=0: Liquidity = 1000
- T=100: Liquidity drops to 0
- T=200: Liquidity increases to 2000
- T=300: Observation is taken

With our current implementation:
- From T=0 to T=100, we correctly accumulate 100 seconds worth of liquidity.
- From T=100 to T=200, we incorrectly accumulate 100 seconds as if there was 1 unit of liquidity.
- From T=200 to T=300, we correctly accumulate 100 seconds worth of liquidity.

This results in an overestimation of the average liquidity over this period.

## Fix

Modify the function to skip accumulation when liquidity is zero.

```solidity
function transform(
    Observation memory last,
    uint32 blockTimestamp,
    int24 tick,
    uint128 liquidity
) private pure returns (Observation memory) {
    unchecked {
        uint32 delta = blockTimestamp - last.blockTimestamp;
Uninitialized Oracle Slots During Expansion Cause Data Gaps and Inaccurate AMM Readings
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/main/contracts/CL/core/libraries/Oracle.sol#L74


# Vulnerability details

## Description

The Oracle library is designed to maintain a circular buffer of observations for tracking historical price and liquidity data in an AMM pool. The write function is responsible for adding new observations to this buffer.

The write function has a flaw in its logic for increasing the cardinality (size) of the observation array. When increasing the cardinality, it fails to properly initialize new slots, which can lead to gaps in the data or reading of uninitialized data.

## Code

```solidity
function write(
    Observation[65535] storage self,
    uint16 index,
    uint32 blockTimestamp,
    int24 tick,
    uint128 liquidity,
    uint16 cardinality,
    uint16 cardinalityNext
) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) {
    unchecked {
        Observation memory last = self[index];

        if (last.blockTimestamp == blockTimestamp) return (index, cardinality);

        if (cardinalityNext > cardinality && index == (cardinality - 1)) {
            cardinalityUpdated = cardinalityNext;
        } else {
            cardinalityUpdated = cardinality;
        }

        indexUpdated = (index + 1) % cardinalityUpdated;
        self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity);
    }
}
```

The issue lies in how the function handles increasing the cardinality (the number of observations stored). When `cardinalityNext` is greater than `cardinality`, the function immediately jumps to using the new, larger cardinality without initializing the new slots in the observation array.


a. Cardinality Increase Condition:
```solidity
if (cardinalityNext > cardinality && index == (cardinality - 1)) {
    cardinalityUpdated = cardinalityNext;
} else {
    cardinalityUpdated = cardinality;
}
```
This condition checks if we should increase the cardinality. It does so when `cardinalityNext` is larger and we've filled the current array (index is at the end).

b. Index Update:
```solidity
indexUpdated = (index + 1) % cardinalityUpdated;
```
This calculates the next index to write to, wrapping around based on the new cardinality.

c. Writing the New Observation:
```solidity
self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity);
```
This writes the new observation to the calculated index.

When we increase cardinality, say from 5 to 10, the function will jump from using indices 0-4 to suddenly writing to index 5, leaving indices 6-9 uninitialized. This creates gaps in our observation history.

## Impact

This bug can lead to several serious issues:

a. Data Gaps: Large portions of the observation array may be left uninitialized, creating gaps in historical data.

b. Incorrect Readings: Functions that read from these uninitialized slots (like `observe` or `observeSingle`) may return incorrect or zero values, leading to inaccurate price or liquidity cal
Fee collection pool doesn't match the distribution gauge
Audit: 2024-10-ramses-exchange-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/gauge/FeeCollector.sol#L77-L87
https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/gauge/FeeCollector.sol#L107-L111


# Vulnerability details

## Proof of Concept
Let's consider the `collectProtocolFees` function specifically the handling of redirected pools and gauges.

https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/gauge/FeeCollector.sol#L77-L87

```solidity
/// @dev check if there's a gauge
IVoter _voter = voter;
address gauge = _voter.gaugeForPool(address(pool));
bool isAlive = _voter.isAlive(gauge);

/// @dev check if it's a cl gauge redirected to another gauge
if (gauge == address(0) || !isAlive) {
    address toPool = _voter.poolRedirect(address(pool));
    gauge = _voter.gaugeForPool(address(toPool));
    isAlive = _voter.isAlive(gauge);
}
```

The issue here iss that when a pool is redirected, the function updates the `gauge` and `isAlive` variables, but it continues to use the original `pool` for fee collection. This could cause a mismatch between the pool from which fees are collected and the gauge/fee distributor to which those fees are sent.

Specifically:
-- If the original pool has no gauge or an inactive gauge, the function looks for a redirected pool.
-- It then gets the gauge for the redirected pool and checks if it's alive.
-- But when it comes to actually collecting the fees, [it still uses the original `pool` object](https://github.com/code-423n4/2024-10-ramses-exchange/blob/236e9e9e0cf452828ab82620b6c36c1e6c7bb441/contracts/CL/gauge/FeeCollector.sol#L107-L111).

```solidity
pool.collectProtocol(
    address(this),
    type(uint128).max,
    type(uint128).max
);
```

This means that fees are being collected from the original pool but distributed based on the gauge of a different (redirected) pool. This mismatch could lead to incorrect fee distribution or some other unexpected issue.


## Recommendations
1. Collect fees from the redirected pool if one is found, or
2. Not use the redirected pool's gauge at all and treat the original pool as having no valid gauge if its own gauge is not active.


## Assessed type

Context