xanoutas/solidity-security-findings huggingface dataset:
MintableIncentivizedERC721 and NToken do not comply with ERC721, breaking composability
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L572


# Vulnerability details

## Description

MintableIncentivizedERC721 implements supportsInterface as below:
```
/**
 * @dev See {IERC165-supportsInterface}.
 */
function supportsInterface(bytes4 interfaceId)
    external
    view
    virtual
    override(IERC165)
    returns (bool)
{
    return
        interfaceId == type(IERC721Enumerable).interfaceId ||
        interfaceId == type(IERC721Metadata).interfaceId;
}
```

The issue is that it will only support the ERC721 extensions, and does not comply with ERC721 itself. From [EIP721](https://eips.ethereum.org/EIPS/eip-721): 
"Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces (subject to “caveats” below):"
```
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
...
```

interface IDs are calculating by XORing together all the function signatures in the interface. Therefore, returning true for IERC721Enumerable and IERC721Metadata will not implicitly include IERC721. 

## Impact

Any contract that will make sure it is dealing with an ERC721 compliant NFT will not interoperate with MintableIncentivizedERC721 and NTokens. Marketplaces and any NFT facilities will not operate with NTokens.

## Proof of Concept

The test below is a standalone POC to show IncentivizedERC721 does not comply with ERC721. It is quickly checkable in Remix, copy deployment address of IncentivizedERC721 to the TestCompliance calls.

```
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.10;
interface IERC721 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed tokenId
    );

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(
        address indexed owner,
        address indexed approved,
        uint256 indexed tokenId
    );

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requiremen
Oracle will become invalid much faster than intended on non-mainnet chains
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

NFTFloorOracle is in charge of answering price queries for ERC721 assets. 
EXPIRATION_PERIOD constant is the max amount of blocks allowed to have passed for the reading to be considered up to date:
```
uint256 diffBlock = currentBlock - priceInfo.updatedAt;
if (diffBlock <= config.expirationPeriod) {
    validPriceList[validNum] = priceInfo.twap;
    validNum++;
}
```

We can see it is set to 1800, which is intended to be 6 hours with 12 second block time assumption:
```
//expirationPeriod at least the interval of client to feed data(currently 6h=21600s/12=1800 in mainnet)
//we do not accept price lags behind to much
uint128 constant EXPIRATION_PERIOD = 1800;
```

The issue is that different blockchains have wildly different block times. BSC has one every 3 seconds, while Avalanche has a new block every 1 sec. Also the block product rate may be subject to future changes. Therefore, readings may be considered stale much faster than intended on non-mainnet chains. 

The correct EVM compatible way to check it is using the block.timestamp variable. Make sure the difference between current and previous timestamp is under 6 * 3600 seconds.

Paraspace docs show they are clearly intending to deploy in multiple chains so it's very relevant.

## Impact

Oracle will become invalid much faster than intended on non-mainnet chains

## Tools Used

Manual audit

## Recommended Mitigation Steps

Use block.timestamp to measure passage of time.
Fallback oracle is unusable when primary oracle is not updated
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

Paraspace implemented their own Oracle wrapper in ParaSpaceOracle.sol. The important function getAssetPrice() is used by many logic functions like health check.

```
function getAssetPrice(address asset)
    public
    view
    override
    returns (uint256)
{
    if (asset == BASE_CURRENCY) {
        return BASE_CURRENCY_UNIT;
    }
    uint256 price = 0;
    IEACAggregatorProxy source = IEACAggregatorProxy(assetsSources[asset]);
    if (address(source) != address(0)) {
        price = uint256(source.latestAnswer());
    }
    if (price == 0 && address(_fallbackOracle) != address(0)) {
        price = _fallbackOracle.getAssetPrice(asset);
    }
    require(price != 0, Errors.ORACLE_PRICE_NOT_READY);
    return price;
}
```

The oracle answer is queried from "source" which in the case of ERC721 tokens, is a ERC721OracleWrapper. It goes to its latestAnswer function:

```
function latestAnswer() external view override returns (int256) {
    return int256(oracleAddress.getPrice(asset));
}
```

oracleAddress is an NFTFloorOracle:
`oracleAddress = INFTFloorOracle(_oracleAddress);`

```
function getPrice(address _asset)
    external
    view
    override
    returns (uint256 price)
{
    uint256 updatedAt = assetPriceMap[_asset].updatedAt;
    require(
        (block.number - updatedAt) <= config.expirationPeriod,
        "NFTOracle: asset price expired"
    );
    return assetPriceMap[_asset].twap;
}
```

In getPrice, if it has been longer than expirationPeriod since last update, the function will revert. Reverts will bubble up all the way to the original getAssetPrice call, which will revert. However, this is not the intended behavior. The aim was to use the fallbackOracle in case of primary oracle not being updated:

```
IEACAggregatorProxy source = IEACAggregatorProxy(assetsSources[asset]);
if (address(source) != address(0)) {
    price = uint256(source.latestAnswer());
}
if (price == 0 && address(_fallbackOracle) != address(0)) {
    price = _fallbackOracle.getAssetPrice(asset);
}
require(price != 0, Errors.ORACLE_PRICE_NOT_READY);
return price;
```

This occured because the standard Chainlink oracle latestAnswer does not revert, rather it [returns](https://etherscan.io/address/0x32dbd3214ac75223e27e575c53944307914f7a90#code) 0:
```
function latestAnswer()
    external
    view
    returns (int256)
  {
    return currentAnswers[latestCompletedAnswer];
  }
```

Therefore, when changing to the home-made oracle, the way to wrap latestAnswer must change as well.

## Impact

fallbackOracle will not be queried when the NFTFloorOracle does not have an updated answer. Most of the pool functions will not operate as health check is impaired.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Wrap the latestAnswer query in a try/catch statem
WPunk will become locked if liquidator ops to receive the underlying token instead of nToken
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/ui/WPunkGateway.sol#L102-L117


# Vulnerability details

## Impact

WPunks received directly as the result of liquidation will be unable to be withdrawn

## Proof of Concept

    function withdrawPunk(uint256[] calldata punkIndexes, address to)
        external
        nonReentrant
    {
        INToken nWPunk = INToken(
            Pool.getReserveData(address(WPunk)).xTokenAddress
        );
        for (uint256 i = 0; i < punkIndexes.length; i++) {
            nWPunk.safeTransferFrom(msg.sender, address(this), punkIndexes[i]);
        }
        Pool.withdrawERC721(address(WPunk), punkIndexes, address(this));
        for (uint256 i = 0; i < punkIndexes.length; i++) {
            WPunk.burn(punkIndexes[i]);
            Punk.transferPunk(to, punkIndexes[i]);
        }
    }

When a user tries to withdraw a CryptoPunk from WPunkGateway it only works if the WPunk is wrapped in an nToken. When calling liquidateERC721 it allows the liquidator to specify whether they want to receive the collateral as an nToken or as the underlying. If the liquidator specifies that they want to receive it as an underlying they will receive the underlying WPunk. Since they now have a WPunk rather than an nWPUnk they cannot withdraw their CyptoPunk and it is frozen in WPunkGateway unable to be withdrawn.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Create a secondary function that allows WPunks to be withdrawn directly or block liquidators from being able to select to receive the underlying for WPunks.
MintableIncentivizedERC721 incorrectly implements safe transfers
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L290-L301
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L320-L327


# Vulnerability details

## Impact
MintableIncentivizedERC721 incorrectly implements safeTransfer and safeTransferFrom by simply replicating the unsafe transfer/transferFrom function.

Raising as medium because as a consequence of this, these ERC721 tokens may end up locked in contracts that does not support ERC-721 tokens while at the same time offering the false impression of this event being impossible to whoever is using the safeTransfer/safeTransferFrom functions.

## Proof of Concept
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L290-L301
https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L320-L327

## Tools Used

## Recommended Mitigation Steps
Either implement safe transfer for safeTransfer/safeTransferFrom function or revert on safeTransfer/safeTransferFrom if not willing to support safe tansfers.
Price can deviate by much more than maxDeviationRate
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

NFTFloorOracle retrieves ERC721 prices for ParaSpace. maxPriceDeviation is a configurable parameter, which limits the change percentage from current price to a new feed update. 

```
function _checkValidity(address _asset, uint256 _twap)
    internal
    view
    returns (bool)
{
    require(_twap > 0, "NFTOracle: price should be more than 0");
    PriceInformation memory assetPriceMapEntry = assetPriceMap[_asset];
    uint256 _priorTwap = assetPriceMapEntry.twap;
    uint256 _updatedAt = assetPriceMapEntry.updatedAt;
    uint256 priceDeviation;
    //first price is always valid
    if (_priorTwap == 0 || _updatedAt == 0) {
        return true;
    }
    priceDeviation = _twap > _priorTwap
        ? (_twap * 100) / _priorTwap
        : (_priorTwap * 100) / _twap;
    // config maxPriceDeviation as multiple directly(not percent) for simplicity
    if (priceDeviation >= config.maxPriceDeviation) {
        return false;
    }
    return true;
}
```

The issue is that it only mitigates a massive change in a single transaction, but attackers can still just call setPrice() and update the price many times in a row, each with maxDevicePrice in price change. 

The correct approach would be to limit the TWAP growth or decline over some timespan. This will allow admins to react in time to a potential attack and remove the bad price feed.

## Impact

Price can deviate by much more than maxDeviationRate

## Proof of Concept

maxDeviationRate = 150
Currently 3 oracles in place, their readings are [1, 1.1, 2]
Oracle #2 can call setPrice(1.5), setPrice(1.7), setPrice(2) to immediately change the price from 1.1 to 2, which is almost 100% growth, much above 50% allowed growth.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Code already has lastUpdated timestamp for each oracle. Use the elapsed time to calculate a reasonable maximum price change per oracle.
Pausing assets only affects future price updates, not previous malicious updates.
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

NFTFloorOracle retrieves ERC721 prices for ParaSpace. It is pausable by admin on a per asset level using setPause(asset, flag).
setPrice will not be callable when asset is paused:
```
function setPrice(address _asset, uint256 _twap)
    public
    onlyRole(UPDATER_ROLE)
    onlyWhenAssetExisted(_asset)
    whenNotPaused(_asset)
```

However, getPrice() is unaffected by the pause flag. This is really dangerous behavior, because there will be 6 hours when the current price treated as valid, although the asset is clearly intended to be on lockdown. 

Basically, pauses are only forward facing, and whatever happened is valid. But, if we want to pause an asset, something fishy has already occured, or will occur by the time setPause() is called. So, "whatever happened happened" mentality is overly dangerous.

## Impact

Pausing assets only affects future price updates, not previous malicious updates.

## Tools Used

Manual audit

## Recommended Mitigation Steps

Add whenNotPaused to getPrice() function as well.
Asset removal leaks previous asset prices which will be used again when asset is re-added.
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

NFTFloorOracle retrieves ERC721 prices for ParaSpace. Recordings of prices are managed in assetFeederMap, mapping between address and FeederRegistrar:
```
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;
}
```
Note that feederPrice is a mapping between feeder and a price read.

```
struct PriceInformation {
    // last reported floor price(offchain twap)
    uint256 twap;
    // last updated blocknumber
    uint256 updatedAt;
    // last updated timestamp
    uint256 updatedTimestamp;
}
```

When an asset is removed, the assetFeederMap entry for that asset is deleted. 
```
function _removeAsset(address _asset)
    internal
    onlyWhenAssetExisted(_asset)
{
    uint8 assetIndex = assetFeederMap[_asset].index;
    delete assets[assetIndex];
    delete assetPriceMap[_asset];
    delete assetFeederMap[_asset];
    emit AssetRemoved(_asset);
}
```

However, it is a known limitation of Solidity that when deleting structures with mappings, the mapping will not be affected at all by the delete. Therefore, feederPrice is leaked and the next time that asset will be added, readings from before will be used. Clearly this is not intended and can lead to asset price report being very different from real price. If the price reading was misbehaving and the asset was removed and readded, the old, bad prices will be valid again.

## Impact

Asset removal leaks previous asset prices which will be used again when asset is readded.

## Proof of Concept

Please copy this test to NFTFloorOracle.t.sol. It shows that the reading from updater[0] is used from before delete/add, after updater[1] and updater[2] add their price.
```
function testLeakedAssetFeederPrice() public {
    address unknown = 0x0000000000000000000000000000000000000001;
    uint256[] memory twaps = new uint256[](1);
    twaps[0] = 1_000;
    uint256[] memory twaps2 = new uint256[](1);
    twaps2[0] = 1_100;
    uint256[] memory twaps3 = new uint256[](1);
    twaps3[0] = 900;
    address[] memory _tokens = new address[](1);
    _tokens[0] = unknown;
    //admin add asset
    cheats.prank(admin);
    _contract.addAssets(_tokens);
    cheats.prank(updaters[0]);
    _contract.setMultiplePrices(_tokens, twaps);
    assertEq(_contract.assets(2), unknown);
    //admin remove asset
    cheats.prank(admin);
    _contract.removeAsset(unknown);
    //admin add asset again
    cheats.prank(admin);
    _contract.addAssets(_tokens);
    cheats.prank(updaters[1]);
    _contract.setMultiplePrices(_tokens, twaps2);
    // Now we'll set the 3rd price and see the leaked 1000 is now the median
    chea
PoolAdmin can steal NFT from NTokens
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/NToken.sol#L168-L186


# Vulnerability details

## Impact

The `executeAirdrop` function allow pool admin to execute arbitrary call to arbitrary contract, including a `transferFrom` call to the underlying NFT contract. This can be used by the pool admin to steal NFT inside the NToken contracts.

Since the `rescueERC721` have a check to make sure the target is not _underlyingAsset, it seems like `executeAirdrop` should have the same check.

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/NToken.sol#L141-L144

```solidity
        require(
            token != _underlyingAsset,
            Errors.UNDERLYING_ASSET_CAN_NOT_BE_TRANSFERRED
        );
```

## Proof of Concept

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/NToken.sol#L168-L186

```solidity
    function executeAirdrop(
        address airdropContract,
        bytes calldata airdropParams
    ) external override onlyPoolAdmin {
        require(
            airdropContract != address(0),
            Errors.INVALID_AIRDROP_CONTRACT_ADDRESS
        );
        require(airdropParams.length >= 4, Errors.INVALID_AIRDROP_PARAMETERS);

        // call project airdrop contract
        Address.functionCall(
            airdropContract,
            airdropParams,
            Errors.CALL_AIRDROP_METHOD_FAILED
        );

        emit ExecuteAirdrop(airdropContract);
    }
```

## Recommended Mitigation Steps
```solidity
        require(
            airdropContract != _underlyingAsset,
            Errors.UNDERLYING_ASSET_CAN_NOT_BE_TRANSFERRED
        );
```
Oracle does not treat upward and downward price movement the same in validity checks, causing safety issues in oracle usage.
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

NFTFloorOracle retrieves ERC721 prices for ParaSpace. maxPriceDeviation is a configurable parameter, which limits the change percentage from current price to a new feed update. We can see how priceDeviation is calculated and compared to maxPriceDeviation in \_checkValidity:

```
priceDeviation = _twap > _priorTwap
    ? (_twap * 100) / _priorTwap
    : (_priorTwap * 100) / _twap;
// config maxPriceDeviation as multiple directly(not percent) for simplicity
if (priceDeviation >= config.maxPriceDeviation) {
    return false;
}
return true;
```

The large number minus small number must be smaller than maxPriceDeviation. However, the way it is calculated means price decrease is much more sensitive and likely to be invalid than a price increase.

10 -> 15, priceDeviation = 15 / 10 = 1.5
15 -> 10, priceDeviation = 15 / 10 = 1.5

From 10 to 15, price rose by 50%. From 15 to 10, price  dropped by 33%. Both are the maximum change that would be allowed by deviation parameter. The effect of this behavior is that the protocol will be either too restrictive in how it accepts price drops, or too permissive in how it accepts price rises.

## Impact

Oracle does not treat upward and downward price movement the same in validity checks, causing safety issues in oracle usage.

## Proof of Concept

## Tools Used

Manual audit

## Recommended Mitigation Steps

Use a percentage base calculation for both upward and downward price movements.
Compromised admin can instantly take all NFTs held in NToken contracts
Audit: 2022-11-paraspace-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/c6820a279c64a299a783955749fdc977de8f0449/paraspace-core/contracts/protocol/tokenization/NToken.sol#L168


# Vulnerability details

## Description

executeAirdrop() is a function admin may call in order to collect airdrops for NFTs held in ParaSpace's nToken contract. 

```
function executeAirdrop(
    address airdropContract,
    bytes calldata airdropParams
) external override onlyPoolAdmin {
    require(
        airdropContract != address(0),
        Errors.INVALID_AIRDROP_CONTRACT_ADDRESS
    );
    require(airdropParams.length >= 4, Errors.INVALID_AIRDROP_PARAMETERS);
    // call project airdrop contract
    Address.functionCall(
        airdropContract,
        airdropParams,
        Errors.CALL_AIRDROP_METHOD_FAILED
    );
    emit ExecuteAirdrop(airdropContract);
}
```

The way the function is implemented, admin can call any function with any parameters, making it an incredible rugging vector risk. Admin has no issue calling ERC721 transfer operation on any of it's held assets and completely stealing the underlying value of the NToken.

## Impact

Compromised admin can instantly take all NFTs held in NToken contracts

## Proof of Concept

## Tools Used

Manual audit

## Recommended Mitigation Steps

executeAirdrop should be refactored to be time-locked. The first call will lock the arguments into place, then after X time has passed the second call will execute it.
In this way, users may withdraw their NFTs from the contract before it is too late.