xanoutas/solidity-security-findings huggingface dataset:
Missing `address(0)` check in the `Payments::pay` function.
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-operation-contracts/blob/27f9d28e00958bf3494fa405a8a5acdcd5ecdc5d/src/aggregate-router/modules/Payments.sol#L26


# Vulnerability details

## Proof of Concept
In the `Payments::pay` function, the fund is sent to the address `recipient` but no validation has been done to check the contents of this input, so any address can be supplied, even the address `address(0)` which is an invalid address. Any funds sent to this address will be burnt and can never be recovered.
```solidity
    function pay(address token, address recipient, uint256 value) internal {
    // @audit lack of address(0) check to recipient input
    if (token == Constants.ETH) {
      recipient.safeTransferETH(value);
    } else {
      if (value == Constants.CONTRACT_BALANCE) {
        value = ERC20(token).balanceOf(address(this));
      }

      ERC20(token).safeTransfer(recipient, value);
    }
  }
```

## Impact
Loss of funds: all funds sent to `address(0)` will be burnt.

## Recommended Mitigation Steps
```diff
    function pay(address token, address recipient, uint256 value) internal {
+   require(recipient != address(0), "Invalid recipient address");
    if (token == Constants.ETH) {
      recipient.safeTransferETH(value);
    } else {
      if (value == Constants.CONTRACT_BALANCE) {
        value = ERC20(token).balanceOf(address(this));
      }

      ERC20(token).safeTransfer(recipient, value);
    }
  }
```



## Assessed type

Invalid Validation
`initialize` will never execute in `KatanaV3Factory` and `KatanaV3Pool`
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/core/KatanaV3Factory.sol#L46-L47
https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/core/KatanaV3Pool.sol#L117


# Vulnerability details

## Proof of Concept
This vulnerability can be found in both `KatanaV3Factory::initialize` and `KatanaV3Pool::initializeImmutables`

However, let us take a look at `KatanaV3Factory::initialize` from the code below, we can observe that in the constructor `beacon=address(1)` which causes  `initialize` not to execute but revert on the require statement. 

why? This is because when you look at the function the first line in the body is a require statement that makes sure our `beacon==address(0)` but from our constructor you can see the beacon is already initialised to address(1) there by causing the require statement to always revert.

```solidity
  constructor() {
    // disable initialization
>    beacon = address(1);
  }

  modifier onlyOwner() {
    _checkOwner();
    _;
  }

  function _checkOwner() internal view virtual {
    require(owner == msg.sender, "KatanaV3Factory: FORBIDDEN");
  }
//@audit 
  function initialize(address beacon_, address owner_, address treasury_) external {
>    require(beacon == address(0), "KatanaV3Factory: ALREADY_INITIALIZED");

    require(beacon_ != address(0), "KatanaV3Factory: INVALID_BEACON");
    require(owner_ != address(0), "KatanaV3Factory: INVALID_OWNER");
    require(treasury_ != address(0), "KatanaV3Factory: INVALID_TREASURY");

    // this beacon is treated as immutable
    // so there is no need to emit an event
    beacon = beacon_;

    // owner is also treated as immutable
    owner = owner_;

    treasury = treasury_;
    emit TreasuryChanged(address(0), treasury_);

    _enableFeeAmount(100, 1, 5, 10);
    _enableFeeAmount(3000, 60, 5, 30);
    _enableFeeAmount(10000, 200, 15, 100);
  }
```

When this happens this will cause the `onlyOwner` modifier to always revert since `owner`  has not been initialized and the `_checkOwner` will revert. This further causes all onlyOwner functions to be unusable.

Looking at the `KatanaV3Pool::initializeImmutables` which has the same vulnerability present we can further confirm from the [docs/natspec](https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/core/interfaces/pool/IKatanaV3PoolImmutables.sol#L7-L15) we can confirm that the `initializeImmutables` is meant to be called once during deployment.

but from our analysis earlier we can also conclude that this function will never run because of the [constructor](https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/core/KatanaV3Pool.sol#L111-L114) which sets `factory=address(1)` and the require statement that will always revert because `factory` is not address(0).


## Recommended Mitigation Steps
Implement a new logic
Missing `address(0)` while initializing the `PeripheryImmutableState` in his constructor.
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/base/PeripheryImmutableState.sol#L17


# Vulnerability details

## Proof of Concept
In the `constructor` of the `PeripheryImmutableState` contract, when it was initialized, no validation was performed, so the `_factory` parameter can be an `address(0)` and if it is, even the `governance` variable will be an `address(0)` ; The EVM will treat `address(0)` as if it implements the `IKatanaV3Factory` interface, even though `address(0)` does not have any code or contract associated with it. This will lead to unexpected behavior.
```solidity
    constructor(address _factory, address _WETH9) {
        // @audit missing address(0) check
@>      factory = _factory;
        WETH9 = _WETH9;
@>      governance = IKatanaV3Factory(_factory).owner();
    }
```

## Recommended Mitigation Steps
```diff
    constructor(address _factory, address _WETH9) {
+       require(_factory != address(0), "Invalid factory address");
        factory = _factory;
        WETH9 = _WETH9;
        governance = IKatanaV3Factory(_factory).owner();
    }
```


## Assessed type

Invalid Validation
NonFungiblePositionManager Is Liable To Underflow
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManager.sol#L401
https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManager.sol#L402


# Vulnerability details

## Proof of Concept

The `NonFungiblePositionManager` is a fork of UniswapV3's `NonFungiblePositionManager`.

In the original UniswapV3 codebase, [we see the following warning](https://github.com/Uniswap/v3-periphery/blob/0682387198a24c7cd63566a2c58398533860a5d1/contracts/NonfungiblePositionManager.sol#L369C9-L370C65):

```solidity
@> // the actual amounts collected are returned
(amount0, amount1) = pool.collect(
  recipient,
  position.tickLower,
  position.tickUpper,
  amount0Collect,
  amount1Collect
);

@> // sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
@> // instead of the actual amount so we can burn the token
(position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);
```

These warnings inform us that when calling `collect` on an underlying pool, the returned `amount0` and `amount1` **can sometimes be smaller** than the specified `amount0Collect` and `amount1Collect` **due to the effects of rounding**. 

Notice that in KatanaV3, [similar logic is used in conjunction with additional fee accounting](https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManager.sol#L395C5-L407C113):

```solidity
// the actual amounts collected are returned
@> (amount0, amount1) = pool.collect(recipient, position.tickLower, position.tickUpper, amount0Collect, amount1Collect);

CollectedFees storage fees = _collectedFees[params.tokenId];
BurnedLiquidityOwed storage burnedLiquidityOwed = _burnedLiquidityOwed[params.tokenId];
// we record only the fees collected, not wholly the tokens owed
@> fees.token0 += amount0 - burnedLiquidityOwed.token0;
@> fees.token1 += amount1 - burnedLiquidityOwed.token1;
(burnedLiquidityOwed.token0, burnedLiquidityOwed.token1) = (0, 0);

// sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
// instead of the actual amount so we can burn the token
(position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);
```

**KatanaV3 erroneously accounts using the real amount of tokens received rather than the expected amount, which is liable to `revert`.**

Let's consider a position which has been burned and has no fees to accrue (i.e. the fees have already been claimed previously). In this scenario, the user's `burnedLiquidityOwed` [will be equal to](https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManag
Permission denied by frontrunning permit
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-operation-contracts/blob/27f9d28e00958bf3494fa405a8a5acdcd5ecdc5d/src/aggregate-router/base/Dispatcher.sol#L22-L203


# Vulnerability details

#### Description

The dispatch function allows multiple commands to be executed based on a given `commandType`, including token transfers via permit and `permitBatch` commands. The use of `permit` is vulnerable to frontrunning because the authorization can be observed on-chain and used by an attacker before the legitimate user, especially in a context where `PERMIT2.permit` or `permit2TransferFrom` commands authorize transfers on behalf of the signatory without enforcing specific conditions. If exploited, an attacker could intercept and execute the token transfer, exhausting the permission before the intended user.

#### Proof of Concept (PoC)

Consider the following scenario, assuming a frontrunning attacker monitors the mempool:

1. Legitimate user (Alice) signs a permit transaction authorizing a token transfer.
2. Alice submits a transaction to the blockchain to invoke dispatch with `commandType` set to `Commands.PERMIT2_TRANSFER_FROM` and provides the required permit authorization in inputs.
3. Attacker (Bob) observes Alice’s pending transaction, copies the signed permit, and submits a similar transaction with a higher gas price, resulting in frontrunning Alice’s transaction.
4. Bob’s transaction succeeds, exhausting Alice’s permit authorization.
5. When Alice’s transaction is mined, it fails because the permit has already been used.

This PoC demonstrates how an attacker can intercept the permit authorization and consume it first, thus invalidating the original user’s intent.

```
// Example PoC Code Snippet
address token = <Token Address>;
address recipient = <Recipient Address>;
uint160 amount = 100;

// Alice signs a permit for token transfer
bytes calldata permitData = abi.encodeWithSelector(PERMIT2.permit.selector, token, recipient, amount);
dispatch(Commands.PERMIT2_TRANSFER_FROM, permitData);

// Bob frontruns the transaction and uses the permit before Alice's transaction is mined.
```

#### Recommendation

You can use something like [trustlessPermit](https://github.com/trust1995/trustlessPermit/blob/main/TrustlessPermit.sol)


## Assessed type

DoS
Wrong implementation of accesss control for KatanaGovernance._authorized()
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

/root/katana-operation-contracts/src/governance/KatanaGovernance.sol#L377-L383


# Vulnerability details

## Proof of Concept

The following implementation of _isAuthorized is wrong since an account is considered authorized if 
```block.timestamp > expiry``` even if ```$.allowed[account] = false```. Meanwhile, even after the authorization expires, if ```$.allowed[account]``` is true, then it is still authorized. 

```javascript 

 function _isAuthorized(Permission storage $, address account) private view returns (bool) {
    uint256 expiry = $.whitelistUntil;
    if (expiry == UNAUTHORIZED) return false;
    if (expiry == AUTHORIZED || block.timestamp > expiry) return true; // but we have not checked the account!

    return $.allowed[account];
  }
```

## Recommended Mitigation Steps
The correct way is to check both expiration time and ```$.allowed[account]``` with a conjunction. 


```diff 

 function _isAuthorized(Permission storage $, address account) private view returns (bool) {
    uint256 expiry = $.whitelistUntil;
    if (expiry == UNAUTHORIZED) return false;
-    if (expiry == AUTHORIZED || block.timestamp > expiry) return true; // but we have not checked the account!

+    if(expiry = AUTHORIZED) return true;

-    return $.allowed[account];
-    if(block.timestamp > expiry && $.allowed[account]) return true;

  }
```


## Assessed type

Access Control
NonfungiblePositionManager.increaseLiquidity() has missed the ```isAuthorizedForToken(params.tokenId)``` modifier.
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

/root/katana-v3-contracts/src/periphery/NonfungiblePositionManager.sol#L247-L295


# Vulnerability details

## Proof of Concept
NonfungiblePositionManager.increaseLiquidity() has missed the ```isAuthorizedForToken(params.tokenId)``` modifier. As a result, an authorized user can deploy a contract and uses it to call ```NonfungiblePositionManager.increaseLiquidity()``` to increase the liquidity for another liquidity provider. 

```javascript 
  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];

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

    bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);

    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);

    position.tokensOwed0 += uint128(
      FullMath.mulDiv(
        feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128
      )
    );
    position.tokensOwed1 += uint128(
      FullMath.mulDiv(
        feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128
      )
    );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    position.liquidity += liquidity;

    emit IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1);
  }
```

For example given a position with ```tokenId``` owned by  user A, an unauthorized user B can deploy a contract contractB and uses it to call  ```NonfungiblePositionManager.increaseLiquidity()``` to increase the liquidity for user A even uses A does not want it. 


Impact: 1) anybody can increase the liquidity for another user; 2) Since there is no limit for how much liquidity one can add, one might just add 1 wei of liquidity to another user so that his position can never be burned (using decreaseLiquidity() and then collect()). 


## Recommended Mitigation Steps
Add the modifier  ```isAuthorizedForToken(params.tokenId)```  to the ```NonfungiblePositionManager.increaseLiquidity()``` function. 






## Assessed type

Access Control
Divide-before-multiplication loss in Oracle.observseSingle() leads to inaccurate results for tickCumulative for the KatanaV3Pool contract.
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

KatanaV3Pool.sol#L205
KatanaV3Pool.sol#L358
KatanaV3Pool.sol#L660


# Vulnerability details

## Proof of Concept
The finding is in file Oracle, however, it has impact to KatanaV3Pool.sol,  it is valid according to "If you discover a bug in any contract or library outside of the files listed above that impact following contracts, we will consider the issue valid".


First, we notice that the following function has a divide-before-multiplication precision loss at L263: 

```javascript

Oracle.sol

 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) {
    if (secondsAgo == 0) {
      Observation memory last = self[index];
      if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity);
      return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128);
    }

    uint32 target = time - secondsAgo;

    (Observation memory beforeOrAt, Observation memory atOrAfter) =
      getSurroundingObservations(self, time, target, tick, index, liquidity, cardinality);

    if (target == beforeOrAt.blockTimestamp) {
      // we're at the left boundary
      return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128);
    } else if (target == atOrAfter.blockTimestamp) {
      // we're at the right boundary
      return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128);
    } else {
      // we're in the middle
      uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp;
      uint32 targetDelta = target - beforeOrAt.blockTimestamp;
      return (
        beforeOrAt.tickCumulative
          + ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / observationTimeDelta) * targetDelta,
        beforeOrAt.secondsPerLiquidityCumulativeX128
          + uint160(
            (
              uint256(atOrAfter.secondsPerLiquidityCumulativeX128 - beforeOrAt.secondsPerLiquidityCumulativeX128)
                * targetDelta
            ) / observationTimeDelta
          )
      );
    }
  }

```

POC: the following code shows the precision loss: 

```javascript

 function testDifference() public {

        uint beforeOrAtTickCumulative = 212844;
        uint atOrAfterTickCumulative = 617233;
        uint observationTimeDelta = 562311;
        uint targetDelta = 524799;

        uint tickCumulative_current = beforeOrAtTickCumulative + (((atOrAfterTickCumulative - beforeOrAtTickCumulative) / observationTimeDelta) * (targetDelta));
        uint tickCumulative_better = beforeOrAtTickCumulative + (((atOrAfterTickCumulative - beforeOrAtTickCumulative) * targetDelta) / (observationTimeDelta));
        
        console.log("Current Implementation: ", tickCumulative_current);
        console.log("Better Implementation: ", tickCumulative_better);
    }
ETH Dust Accumulation in `createAndInitializePoolIfNecessary`
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/base/PoolInitializer.sol#L15-L35


# Vulnerability details


## Impact
The `createAndInitializePoolIfNecessary` function in the PoolInitializer contract is marked as payable, but does not include logic for handling any ETH sent with the transaction. This oversight could result in unintended "dust" ETH becoming trapped in the contract indefinitely. Over time, this could lead to the following impacts:
## Proof of Concept
1. In the `PoolInitializer` contract:
   - [`createAndInitializePoolIfNecessary`](https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/base/PoolInitializer.sol#L15-L35) is marked `payable`, allowing it to receive ETH.
   - This function neither uses nor refunds any ETH sent with the transaction, causing any ETH attached to calls to be trapped in the contract.
  
2. Potential Issues:
   - **Trapped ETH**: ETH sent to this function remains in the contract without a clear mechanism to withdraw, affecting users and creating “dust” balances.
   - **Unintended Accumulation**: Repeated calls could accumulate dust ETH balances, which could be at risk if an attacker gains unauthorized access to withdraw from the contract.

## Tools Used
Manual code review.

## Recommended Mitigation Steps
Add a mechanism to refund any unused ETH at the end of the function. For instance, implementing a call to the `refundETH()` function from the `PeripheryPayments` contract, if available, would prevent ETH from being trapped in the contract and ensure user funds are safely returned. Alternatively, adding an `address(0).call{value: msg.value}("");` call would return any attached ETH back to the sender if unutilized.



## Assessed type

Payable
Incorrect handling of Fee Accounting
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/core/KatanaV3Pool.sol#L781-L790


# Vulnerability details

## Proof of Concept
```solodity
      if (paid0 > 0) {
        uint256 fees0 = FullMath.mulDiv(paid0, slot0.feeProtocolNum, slot0.feeProtocolDen);
        if (fees0 > 0) TransferHelper.safeTransfer(_token0, treasury, fees0);
        feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);
      }
      if (paid1 > 0) {
        uint256 fees1 = FullMath.mulDiv(paid1, slot0.feeProtocolNum, slot0.feeProtocolDen);
        if (fees1 > 0) TransferHelper.safeTransfer(_token1, treasury, fees1);
        feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
      }
```

The fee accounting is incorrect because fees from flash loans are transferred directly to the treasury after each flash loan, which also results in an increase in the global liquidity tracker. This poses a problem, as users may be unable to withdraw the fees they are entitled to due to a lack of liquidity, since the fees have exited the protocol. All fees recorded in the global tracker should remain within the protocol.

## Recommended Mitigation Steps
If the protocol intends to send all fees to the treasury, it should do so without updating the feeGrowthGlobal tracker. This approach will ensure accurate accounting, as the fees generated from flash loans will be allocated to the treasury rather than to users.





## Assessed type

Other
require nextId to be zero will always revert due to nextId already initialized to 1
Audit: 2024-10-ronin-findings
Severity: MEDIUM

# Lines of code

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManager.sol#L104


# Vulnerability details

In NonfungiblePositionManager.sol there will be a problem initializing.


```solidity
  /// @dev The ID of the next token that will be minted. Skips 0
  uint176 private _nextId = 1;
  /// @dev The ID of the next pool that is used for the first time. Skips 0
  uint80 private _nextPoolId = 1;
```

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManager.sol#L83


_nextId is initialized to 1 in state variable declaration



```solidity
  function initialize() external {
    require(_nextId == 0, "Already initialized");

    _nextId = 1;
    _nextPoolId = 1;
  }
```

https://github.com/ronin-chain/katana-v3-contracts/blob/03c80179e04f40d96f06c451ea494bb18f2a58fc/src/periphery/NonfungiblePositionManager.sol#L104


Initialize function checks if it's 0


This means that the check will always fail to initialize 



## Recommended Mitigation Steps

```solidity

  function initialize() external {
    require(_nextId == 1, "Already initialized");

    _nextId = 1;
    _nextPoolId = 1;
  }
```
Chnage the require statement to check for _nextId == 1





## Assessed type

Error