xanoutas/solidity-security-findings huggingface dataset:
Replay attacks to bypass high fees
Audit: 2024-01-decent-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/UTB.sol#L228


# Vulnerability details

## Impact
In UTB.sol the protocol fees are maintained via an ECDSA signature, however the [fee collector](https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/UTBFeeCollector.sol#L44) does not use a nonce to prevent signature replay attacks. I would assume that protocol fees are not expected to remain constant throughout the lifetime of the protocol. So a malicious user can call the swap function and save that transaction to replay it a later stage.

The benefit being mainly that should a user need to perform such a swap in the future, they can simply replay the call with the benefit of using a constant swap fee. 


## Proof of Concept

Step 1

User calls [swapAndExecute](https://github.com/code-423n4/2024-01-decent/blob/011f62059f3a0b1f3577c8ccd1140f0cf3e7bb29/src/UTB.sol#L108)

Step 2

User saves transaction

Step 3

User calls swapAndExecute at a later stage for the same transaction.


## Tools Used

Manual Review

## Recommended Mitigation Steps

I would recommend a transaction nonce be added to mitigate such fee manipulation


## Assessed type

Invalid Validation
Attacker can drain pool using executeBuyWithCredit with malicious marketplace payload.
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/marketplaces/LooksRareAdapter.sol#L59
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/libraries/logic/MarketplaceLogic.sol#L397


# Vulnerability details

## Description

Paraspace supports leveraged purchases of NFTs through PoolMarketplace entry points. User calls buyWithCredit with marketplace, calldata to be sent to marketplace, and how many tokens to borrow.
```
function buyWithCredit(
    bytes32 marketplaceId,
    bytes calldata payload,
    DataTypes.Credit calldata credit,
    uint16 referralCode
) external payable virtual override nonReentrant {
    DataTypes.PoolStorage storage ps = poolStorage();
    MarketplaceLogic.executeBuyWithCredit(
        marketplaceId,
        payload,
        credit,
        ps,
        ADDRESSES_PROVIDER,
        referralCode
    );
}
```

In executeBuyWithCredit, orders are deserialized from the payload user sent to a DataTypes.OrderInfo structure. Each MarketplaceAdapter is required to fulfil that functionality through getAskOrderInfo:
```
DataTypes.OrderInfo memory orderInfo = IMarketplace(marketplace.adapter)
    .getAskOrderInfo(payload, vars.weth);
```

If we take a look at LooksRareAdapter's getAskOrderInfo, it will the consideration parameter using only the MakerOrder parameters, without taking into account TakerOrder params
```
(
    OrderTypes.TakerOrder memory takerBid,
    OrderTypes.MakerOrder memory makerAsk
) = abi.decode(params, (OrderTypes.TakerOrder, OrderTypes.MakerOrder));
orderInfo.maker = makerAsk.signer;

consideration[0] = ConsiderationItem(
    itemType,
    token,
    0,
    makerAsk.price, // TODO: take minPercentageToAsk into account
    makerAsk.price,
    payable(takerBid.taker)
);
```

The OrderInfo constructed, which contains the consideration item from maker, is used in \_delegateToPool, called by \_buyWithCredit(), called by executeBuyWithCredit:

```
for (uint256 i = 0; i < params.orderInfo.consideration.length; i++) {
    ConsiderationItem memory item = params.orderInfo.consideration[i];
    require(
        item.startAmount == item.endAmount,
        Errors.INVALID_MARKETPLACE_ORDER
    );
    require(
        item.itemType == ItemType.ERC20 ||
            (vars.isETH && item.itemType == ItemType.NATIVE),
        Errors.INVALID_ASSET_TYPE
    );
    require(
        item.token == params.credit.token,
        Errors.CREDIT_DOES_NOT_MATCH_ORDER
    );
    price += item.startAmount;
}
```

The total price is charged to msg.sender, and he will pay it with debt tokens + immediate downpayment. 
After enough funds are transfered to the Pool contract, it delegatecalls to the LooksRare adapter, which will do the actual call to LooksRareExchange. The exchange will send the money gathered in the pool to maker, and give it the NFT.

The issue is that attacker can su
Some arbitrary feeders will not be removable, even by admin.
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L332


# Vulnerability details

## Description

NFTFloorOracle receives data from different "feeders". They are added using addFeeders() and removed by removeFeeder().

Feeders are managed by two data structures.
feeders is an array, each element in the address of the feeder.
feederPositionMap maps between feeder address to its position.
```
    /// @dev All feeders
    address[] public feeders;
    /// @dev feeder map
    // feeder address -> index in feeder list
    mapping(address => FeederPosition) private feederPositionMap;
	
	struct FeederPosition {
    // if feeder registered or not
    bool registered;
    // index in feeder list
    uint8 index;
}
```

Let's see how \_removeFeeder  is implemented:
```
function _removeFeeder(address _feeder)
    internal
    onlyWhenFeederExisted(_feeder)
{
    uint8 feederIndex = feederPositionMap[_feeder].index;
    if (feederIndex >= 0 && feeders[feederIndex] == _feeder) {
        feeders[feederIndex] = feeders[feeders.length - 1];
        feeders.pop();
    }
    delete feederPositionMap[_feeder];
    revokeRole(UPDATER_ROLE, _feeder);
    emit FeederRemoved(_feeder);
}
```

To remove feeder, we take its index from postion map, and store in that index the current last feeder in the feeder array. Then the last element is popped, like in a classic swap and pop. The issue is that the feederPositionMap entry for the last element in the array, which was just moved somewhere else, will still have the index pointing to the previous original location. As a result, the feeder can no longer by removed, breaking an important safety guarantee of the system that all feeders can be removed if they misbehave.

## Impact

Some arbitrary feeders will not be removable, even by admin.

## Proof of Concept

Copy the test below to NFTFloorOracle.t.sol:
```
function testAddRemoveFeeders() public {
    address[] memory newUpdaters = new address[](2);
    newUpdaters[0] = 0x0000000000000000000000000000000000000005;
    newUpdaters[1] = 0x0000000000000000000000000000000000000006;
    //add new updaters and remove old ones
    cheats.startPrank(admin);
    _contract.addFeeders(newUpdaters);
    _contract.removeFeeder(newUpdaters[1]);
    _contract.removeFeeder(newUpdaters[0]);
    _contract.addFeeders(newUpdaters);
    _contract.removeFeeder(newUpdaters[0]);
    _contract.removeFeeder(newUpdaters[1]);
}
```

The test shows that feeders can be removed from high to low, but will revert when removed from low to high. This is because when removing the high feeder, the swap logic will swap it with itself, so index variable of other feeders is not harmed.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Change the \_removeFeeder function as below:

```
function _removeFeeder(address _feeder)
    internal
    onlyWhenFeederExisted(_feeder)
{
    uint8 feederIndex
UniswapV3 tokens of certain pairs will be wrongly valued, leading to liquidations.
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/UniswapV3OracleWrapper.sol#L245


# Vulnerability details

## Description

UniswapV3OracleWrapper is responsible for price feed of UniswapV3 NFT tokens. Its getTokenPrice() is used by the health check calculation in GenericLogic.

getTokenPrice gets price from the oracle and then uses it to calculate value of its liquidity.
```
function getTokenPrice(uint256 tokenId) public view returns (uint256) {
    UinswapV3PositionData memory positionData = getOnchainPositionData(
        tokenId
    );
    PairOracleData memory oracleData = _getOracleData(positionData);
    (uint256 liquidityAmount0, uint256 liquidityAmount1) = LiquidityAmounts
        .getAmountsForLiquidity(
            oracleData.sqrtPriceX96,
            TickMath.getSqrtRatioAtTick(positionData.tickLower),
            TickMath.getSqrtRatioAtTick(positionData.tickUpper),
            positionData.liquidity
        );
    (
        uint256 feeAmount0,
        uint256 feeAmount1
    ) = getLpFeeAmountFromPositionData(positionData);
    return
        (((liquidityAmount0 + feeAmount0) * oracleData.token0Price) /
            10**oracleData.token0Decimal) +
        (((liquidityAmount1 + feeAmount1) * oracleData.token1Price) /
            10**oracleData.token1Decimal);
}
```

In `_getOracleData`,  sqrtPriceX96 of the holding is calculated, using square root of token0Price and token1Price, corrected for difference in decimals. In case they have same decimals, this is the calculation:
```
if (oracleData.token1Decimal == oracleData.token0Decimal) {
    // multiply by 10^18 then divide by 10^9 to preserve price in wei
    oracleData.sqrtPriceX96 = uint160(
        (SqrtLib.sqrt(
            ((oracleData.token0Price * (10**18)) /
                (oracleData.token1Price))
        ) * 2**96) / 1E9
    );
```

The issue is that the inner calculation, could be 0, making the whole expression zero, although price is not.
```
((oracleData.token0Price * (10**18)) /
                        (oracleData.token1Price))
```

This expression will be 0 if oracleData.token1Price > token0Price * 10\*\*18. This is not far fetched, as there is massive difference in prices of different ERC20 tokens due to tokenomic models. For example, WETH (18 decimals) is $1300, while BTT (18 decimals) is $0.00000068.

The price is represented using X96 type, so there is plenty of room to fit the price between two tokens of different values. It is just that the number is multiplied by 2\*\*96 too late in the calculation, after the division result is zero.

Back in getTokenPrice, the sqrtPriceX96 parameter which can be zero, is passed to LiquidityAmounts.getAmountsForLiquidity() to get liquidity values. In case price is zero, the liquidity calculator will assume all holdings are amount0, while in reality they could be all amount1, or a combination of the two.

```
function getAmountsForLiquidity(
Downcast can lead to overflow and impact the functionality to remove an asset in `NFTFloorOracle`
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L284
https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L300


# Vulnerability details

## Impact

When adding a new asset, the index for the last asset will be saved and downcasted on [L284](https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L284). If the number of assets is bigger than 256, the downcasting operation will lead to a silent overflow. Consequentially, the incorrect index will be stored.

This can impact the functionality to [remove an asset](https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L300) (because the index for the asset to be removed will be retrieved while deleting an asset).

## Proof of concept

Take the following case:

1. Assume 256 items are added.
2. When adding item #257, the index saved will be zero, due to the overflow. E.g. uint8(257 - 1) = 0
3. While trying to remove asset #257, the asset with index 0 will be removed instead.

On the tests setup, 10 assets are added initialy. The first asset is DOODLE. The following POC demonstrates what would happen if 247 are added after the initial 10 items. Assume items from index 11 to 256 are random addresses (for demonstration), and the asset for index 257 is DAI.

```javascript
import crypto from "crypto"; // To generate random Addresses
import {utils, constants} from "ethers"; // To get constants.AddressZero

  // Copying the following test case into `_oracle_nft_floor_price.spec.ts` should work.

  it("Adding more than 256 assets can break the functionality to remote assets", async () => {
    const { nftFloorOracle, dai} = testEnv;

    // Add assets from index 11 to 256.
    // Note that 10 assets are already added initially on the current tests setup. 
    const addresses: string[] = [];
    for (let i = 11; i <= 256; ++i) {
      // Generate a random address.
      addresses.push(`0x${crypto.randomBytes(20).toString("hex")}`);
    }
    try {
      await nftFloorOracle.addAssets(addresses);
    } catch (e) {}

    // Confirming that the first item is DOODLE.
    const doodleAddress = '0xD6C850aeBFDC46D7F4c207e445cC0d6B0919BDBe';
    expect(await nftFloorOracle.assets(0)).to.equal(doodleAddress);

    // Add DAI as item #257.
    try {
      await nftFloorOracle.addAssets([dai.address]);
    } catch (e) {}

    // Removing DAI will accidentaly remove DOODLE.
    await nftFloorOracle.removeAsset(dai.address);

    // This assertation will pass to demonstrate the bug.
    // First asset will no longer be DOODLE and it will become address zero.
    expect(await nftFloorOracle.assets(0)).to.equal(constants.AddressZero);
  });
```

## Recommended Mitigation Steps

If it's expected for the number of assets to never exeed 256 items, consider adding a [SafeCast](https://docs.openzeppel
NFTFloorOracle's asset and feeder structures can be corrupted
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L278-L286
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L307-L316


# Vulnerability details

NFTFloorOracle's _addAsset() and _addFeeder() truncate the `assets` and `feeders` arrays indices to 255, both using `uint8 index` field in the corresponding structures and performing `uint8(assets.length - 1)` truncation on the new element addition.

`2^8 - 1` looks to be too tight as an **all time** element count limit. It can be realistically surpassed in a couple years time, especially given multi-asset and multi-feeder nature of the protocol. This way this isn't a theoretical unsafe truncation, but an accounting malfunction that is practically reachable given long enough system lifespan, without any additional requirements as asset/feeder turnaround is a going concern state of the system.

## Impact

Once truncation start corrupting the indices the asset/feeder structures will become incorrectly referenced and removal of an element will start to remove another one, permanently breaking up the structures.

This will lead to inability to control these structures and then to Oracle malfunction. This can lead to collateral mispricing. Setting the severity to be medium due to prerequisites.

## Proof of Concept

`feederPositionMap` and `assetFeederMap` use `uint8` indices:

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L32-L48

```solidity
struct FeederRegistrar {
    // if asset registered or not
    bool registered;
    // index in asset list
    uint8 index;
    // if asset paused,reject the price
    bool paused;
    // feeder -> PriceInformation
    mapping(address => PriceInformation) feederPrice;
}

struct FeederPosition {
    // if feeder registered or not
    bool registered;
    // index in feeder list
    uint8 index;
}
```

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L79-L88

```solidity
    /// @dev feeder map
    // feeder address -> index in feeder list
    mapping(address => FeederPosition) private feederPositionMap;

    ...

    /// @dev Original raw value to aggregate with
    // the NFT contract address -> FeederRegistrar which contains price from each feeder
    mapping(address => FeederRegistrar) public assetFeederMap;
```

On entry removal both `assets` array length do not decrease:

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/misc/NFTFloorOracle.sol#L296-L305

```solidity
    function _removeAsset(address _asset)
        internal
        onlyWhenAssetExisted(_asset)
    {
        uint8 assetIndex = assetFeederMap[_
User can pass auction recovery health check easily with flashloan
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/pool/PoolParameters.sol#L281


# Vulnerability details

## Description

ParaSpace features an auction mechanism to liquidate user's NFT holdings and receive fair value. User has the option, before liquidation actually happens but after auction started, to top up their account to above recovery factor (> 1.5 instead of > 1) and use setAuctionValidityTime() to invalidate the auction.
```
require(
    erc721HealthFactor > ps._auctionRecoveryHealthFactor,
    Errors.ERC721_HEALTH_FACTOR_NOT_ABOVE_THRESHOLD
);
userConfig.auctionValidityTime = block.timestamp;
```

The issue is that the check validates the account is topped in the moment the TX is executed. Therefore, user may very easily make it appear they have fully recovered by borrowing a large amount of funds, depositing them as collateral, registering auction invalidation, removing the collateral and repaying the flash loan. Reentrancy guards are not effective to prevent this attack because all these actions are done in a sequence, one finishes before the other begins. However, it is clear user cannot immediately finish this attack below liquidation threshold because health factor check will not allow it.

Still, the recovery feature is a very important feature of the protocol and a large part of what makes it unique, which is why I think it is very significant that it can be bypassed.
I am on the fence on whether this should be HIGH or MED level impact, would support judge's verdict either way.

## Impact

User can pass auction recovery health check easily with flashloan

## Proof of Concept

1. User places NFT as collateral in the protocol
2. User borrows using the NFT as collateral
3. NFT price drops and health factor is lower than liquidation threshold
4. Auction to sell NFT initiates
5. User deposits just enough to be above liquidation threshold
6. User now flashloans 1000 WETH
	1. supply 1000 WETH to the protocol
	2. call setAuctionValidityTime(), cancelling the auction
	3. withdraw the 1000 WETH from the protocol
	4. pay back the 1000 WETH flashloan
7. End result is bypassing of recovery health check

## Tools Used

Manual audit

## Recommended Mitigation Steps

In order to know user has definitely recovered, implement it as a function which holds the user's assets for X time (at least 5 minutes), then releases it back to the user and cancelling all their auctions.
THE FUNCTION LOGIC OF _REMOVEFEEDER() IS FLAWED
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L326-L338


# Vulnerability details

## Impact
When the admin calls `removeFeeder()`, it will internally call `_removeFeeder()` that will have the feeder removed from `feeders` before getting its `UPDATE_ROLE` revoked. In the midst of doing this, the protocol forgets to update `index` of `feederPositionMap` belonging to the last feeder (e.g. `feeder X` for reference) of `feeders` who has now taken over the position of the removed feeder. This could lead to function error when eventually `feederX` will need to be removed too.  

## Proof of Concept
[NFTFloorOracle.sol#L326-L338](https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L326-L338)

```
    function _removeFeeder(address _feeder)
        internal
        onlyWhenFeederExisted(_feeder)
    {
        uint8 feederIndex = feederPositionMap[_feeder].index;
        if (feederIndex >= 0 && feeders[feederIndex] == _feeder) {
            feeders[feederIndex] = feeders[feeders.length - 1];
            feeders.pop();
        }
        delete feederPositionMap[_feeder];
        revokeRole(UPDATER_ROLE, _feeder);
        emit FeederRemoved(_feeder);
    }
```
From the code block above, `if (feederIndex >= 0 && feeders[feederIndex] == _feeder) ` will revert when attempting to remove `feederX` because you are now accessing an out of bound element in `feeders`

## Tools Used
Manual

## Recommended Mitigation Steps
It is recommended updating correctly the `index` of `feederPositionMap` belonging to `feederX` that has replaced the removed feeder.
`NFTFloorOracle` price feeders can be removed by anyone
Audit: 2022-11-paraspace-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L167-L172
https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L326-L338


# Vulnerability details

## Impact

The nfts price feeders in the `NFTFloorOracle` contract should be added or removed only by the `admin` but because the `removeFeeder` function is missing the `onlyRole(DEFAULT_ADMIN_ROLE)` modifier any user can remove a feeder, this could impact the whole protocol if all the feeders for a given nft collection are removed (for example).

## Proof of Concept

The issue occurs in the `removeFeeder` function below :

File: paraspace-core/contracts/misc/NFTFloorOracle.sol [Line 167-172](https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L167-L172)
```
function removeFeeder(address _feeder)
    external
    onlyWhenFeederExisted(_feeder)
{
    _removeFeeder(_feeder);
}
```

As you can see the function contain only the `onlyWhenFeederExisted(_feeder)` modifier and calls the internal function `_removeFeeder` which also contain the same modifier :

File: paraspace-core/contracts/misc/NFTFloorOracle.sol [Line 326-328](https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/misc/NFTFloorOracle.sol#L326-L328)
```
function _removeFeeder(address _feeder) internal onlyWhenFeederExisted(_feeder)
```

So any user can call the `removeFeeder` function to remove an existing feeder and this is wrong because only the `admin` should have the power to remove feeders.


## Tools Used

Manual review

## Recommended Mitigation Steps

To solve this issue the `onlyRole(DEFAULT_ADMIN_ROLE)` modifier must be added to the `removeFeeder` function :

```
function removeFeeder(address _feeder)
    external
    onlyRole(DEFAULT_ADMIN_ROLE)
{
    _removeFeeder(_feeder);
}
```