ParaSpace - NFT Money Market

CertiKFiles named in the report

CertiK reviewed ParaSpace Lending V1 (ParaSpace - NFT Money Market) on 25 Oct 2022: 23 findings, 2 critical; 14 marked fixed, 1 acknowledged, 8 open.

2 critical findings, fixed
Open the reportOpen ParaSpace Lending V1’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings23

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical002
High011
Medium103
Low708
Critical`NToken.permit()` doesn't check the token ownerFixed

```solidity=286 require(owner == ecrecover(digest, v, r, s), Errors.INVALID_SIGNATURE); _nonces[owner] = currentValidNonce + 1; _approve(spender, value); ``` `NToken.permit()` allows the `spender` to transfer in the future the token with id `value` if correctly signed by `owner` message provided. However, it is not checked that `_isApprovedOrOwner(owner, value)`. As a result, anyone can get approval for any token. The `value` argument name is misleading.

CriticalPotential Flashloan AttackFixed

The following check is performed when borrowing if the interest rate mode is stable: ```solidity require( !params.userConfig.isUsingAsCollateral( reservesData[params.asset].id ) || params.reserveCache.reserveConfiguration.getLtv() == 0 || params.amount > IToken(params.reserveCache.xTokenAddress).balanceOf( params.userAddress ), Errors.COLLATERAL_SAME_AS_BORROWING_CURRENCY ); ``` So the current attack only applies if the interest rate mode is variable. The following slightly tweaked attack works in either interest rate mode. Attack Flow: 1. An exploiter creates two contracts A and B. 2. Contract A flash loans 100 ETH from other protocols, it then supplies 100 ETH and is minted 100 ETH of PToken (assuming the asset is ETH), they then borrow 80% of the locked ETH in some other Token. (assuming up to 80% can be borrowed). 3. Contract A transfers the borrowed Token's to Contract B, which then swaps them for around 80 ETH. 4. Contract B then supplies 80 ETH and is minted 80 ETH worth of PToken. Subsequently, they borrow 64 ETH worth of some other Token, which they then swap for around 64 ETH. 5. Contract B uses its own 80 ETH worth of PToken to pay off Contract A's debt. 6. Contract A withdraws all 100 ETH supplied by burning its 100 ETH of PToken, as its debt is now cleared. 7. Contract A repays the 100 ETH flash loan. 9. The exploiter gains around 64 ETH in profit as they still have the 64 ETH in contract B. (Not accounting for the swap and flash loan fees.)

HighPotential Reentrancy AttackFixed

A reentrancy attack can occur when the contract creates a function that makes an external call to another untrusted contract before resolving any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the external call resolved the effects. For example, the hacker can call the `withdrawERC721()` method and use the hook `_checkOnERC721Received` method of the ERC721 receiver to reenter the method `liquidationERC721()`.

MediumFunctions Not RestrictedFixed

```solidity=1 function batchBuyWithCredit( bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, address onBehalfOf, uint16 referralCode ) external payable virtual override nonReentrant { ``` ```solidity function buyWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, address onBehalfOf, uint16 referralCode ) external payable virtual override nonReentrant { ``` Both of the `buyWithCredit()` and `batchBuyWithCredit()` are external functions without any validation. Any user can call them with any parameter. At the same time, these functions do not validate the caller's allowance from the `onBehalfOf` account. Not verifying that the caller has allowance from the `onBehalfOf` account could be a vulnerability.

MediumIncompatibility with Deflationary TokensFixed

When transferring deflationary ERC20 tokens, the input amount may not be equal to the received amount due to the charged transaction fee. For example, if a user sends 100 deflationary tokens (with a 10% transaction fee), only 90 tokens actually arrived at the contract. However, a failure to discount such fees may allow the same user to withdraw 100 tokens from the contract, which causes the contract to lose 10 tokens in such a transaction. Reference: https://thoreum-finance.medium.com/what-exploit-happened-today-for-gocerberus-and-garuda-also-for-lokum-ybear-piggy-caramelswap-3943ee23a39f ```solidity=200 IERC20(token0).safeTransferFrom(sender, address(this), amountAdd0); ``` - Transferring tokens by `amountAdd0`. ```solidity=228 uint256 refund0 = amountAdd0 - amount0; ``` - The `amountAdd0` appears to be used for bookkeeping purposes without compensating the potential transfer fees. --- ```solidity=203 IERC20(token1).safeTransferFrom(sender, address(this), amountAdd1); ``` - Transferring tokens by `amountAdd1`. ```solidity=233 uint256 refund1 = amountAdd1 - amount1; ``` - The `amountAdd1` appears to be used for bookkeeping purposes without compensating the potential transfer fees. --- ```solidity=264 _increaseLiquidityCurrentRange( tokenId, amountAdd0, amountAdd1, amount0Min, amount1Min ); ``` - Transferring tokens by `amountAdd0`. - This function call executes the following operation. - In `NTokenUniswapV3._increaseLiquidityCurrentRange`, - `IERC20(token0).safeTransferFrom(sender, address(this), amountAdd0);` ```solidity=264 _increaseLiquidityCurrentRange( tokenId, amountAdd0, amountAdd1, amount0Min, amount1Min ); ``` - This function call executes the following operation. - In `NTokenUniswapV3._increaseLiquidityCurrentRange`, - `uint256 refund0 = amountAdd0 - amount0;` - The `amountAdd0` appears to be used for bookkeeping purposes without compensating the potential transfer fees.

MediumPotential Financial LossFixed

```solidity=279 if (reserve.xTokenAddress == address(0)) { address underlyingAsset = INToken(token) .UNDERLYING_ASSET_ADDRESS(); reserve = reservesData[underlyingAsset]; bool isNToken = reserve.xTokenAddress == token; require(isNToken, Errors.ASSET_NOT_LISTED); if (!userConfig.isUsingAsCollateral(reserve.id)) { userConfig.setUsingAsCollateral(reserve.id, true); } // No need to supply anymore because it's already NToken continue; } ``` According to the `if` condition `reserve.xTokenAddress == address(0)` of the above statement from method `_repay()`, this means that the purchased token is an `NToken`. However, this token will be locked in the `pool` of the contract forever. This may be incorrect.

LowDiscussion On Borrow With CreditFixed

```solidity=424 // Pool.sol function acceptBidWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, address onBehalfOf, uint16 referralCode ) external virtual override nonReentrant { ``` Currently, the functions `buyWithCredit()`, `batchBuyWithCredit()`, `acceptBidWithCredit()`, and `batchAcceptBidWithCredit()` can be called by anyone and there is no authorization between caller and `onBehalfOf`. In our opinion, these functions would be invoked in relevant gateway contracts. - The functions `buyWithCredit()` and `batchBuyWithCredit()` could be invoked in `WETHGateway.sol`. - The functions `acceptBidWithCredit()` and `batchAcceptBidWithCredit()` could be invoked in `WPunkGateway.sol`. ```solidity // WPunkGateway.sol function acceptBidWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, uint256[] calldata punkIndexes, uint16 referralCode ) external nonReentrant { ...... Pool.acceptBidWithCredit( marketplaceId, payload, credit, msg.sender, referralCode ); } ```

LowIncorrect Conditional StatementFixed

Referring to the comments, the logic may occur in a normal supplyERC721 pool transaction. However, when the operator is `POOL` the transaction will be reverted. That means the `UniswapV3` tokens cannot be supplied in this protocol. ```solidity=47 // if the operator is the pool, this means that the pool is transferring the token to this contract // which can happen during a normal supplyERC721 pool tx if (operator == address(POOL)) { revert(Errors.OPERATION_NOT_SUPPORTED); } ```

LowLack of Account ValidationFixed

```solidity=149 function supplyERC721FromNToken( address asset, DataTypes.ERC721SupplyParams[] calldata tokenData, address onBehalfOf ) external virtual override nonReentrant { SupplyLogic.executeSupplyERC721FromNToken( _reserves, _usersConfig[onBehalfOf], DataTypes.ExecuteSupplyERC721Params({ asset: asset, tokenData: tokenData, onBehalfOf: onBehalfOf, actualSpender: address(0), referralCode: 0 }) ); } ``` In the function `Pool.supplyERC721FromNToken()`, any user can call it to supply ERC721 from other accounts without restriction.

LowMissing Zero Address ValidationFixed

Addresses should be checked before assignment or external call to make sure they are not zero addresses. --- ```solidity=75 (bool success, ) = newImplementation.delegatecall(data); ``` - `newImplementation` is not zero-checked before being used. --- ```solidity=75 _treasury = treasury; ``` - `treasury` is not zero-checked before being used. --- ```solidity=76 _underlyingAsset = underlyingAsset; ``` - `underlyingAsset` is not zero-checked before being used. --- ```solidity=75 _treasury = treasury; ``` - `treasury` is not zero-checked before being used.

Show 4 more
LowPotential Incorrect Price RiskFixed

- GenericLogic.sol contract `_getUserBalanceInBaseCurrency()` method ```solidity uint256 assetPrice; ...... if (INToken(xTokenAddress).getAtomicPricingConfig()) { uint256 totalBalance = INToken(xTokenAddress).balanceOf(user); for (uint256 index = 0; index < totalBalance; index++) { uint256 tokenId = IERC721Enumerable(xTokenAddress) .tokenOfOwnerByIndex(user, index); if ( ICollaterizableERC721(xTokenAddress).isUsedAsCollateral( tokenId ) ) { // TODO use getTokensPrices instead if it saves gas assetPrice = IPriceOracleGetter(oracle).getTokenPrice( currentReserveAddress, tokenId ); balance += assetPrice; } } } ...... unchecked { return (balance / assetUnit, assetPrice); } ``` According to the above statement, the method `_getUserBalanceInBaseCurrency()` will return the `assetPrice` of the last item. And referring to the client's technical documentation, `atomic pricing` is defined as: the first notable difference between typical floor-based and UniV3 is that each token has a different price based on the ERC20 token composition inside the LP token. This means that each UniV3 token has a different price, and in the statement logic, the local variable `assetPrice` will be overwritten by the value of the last item in the loop. We understand that for NFT assets, the user cannot borrow them now. However, the result returned by this method is incorrect and might not make sense.

LowRedundant `else` ClauseFixed

Refer to the `SupplyLogic.executeUseReserveAsCollateral()` method is used to set whether the ERC20 asset can be collateralized, and there is no similar statement for the ERC721 asset, we think the ERC721 asset the "else" statement is redundant.

LowRedundant CodeFixed

The require statement is duplicated in `validateSupplyBase()` with the related statements in `validateSupplyERC20()` and `validateSupplyERC721()`.

LowUnchecked ERC-20 `transfer()`/`transferFrom()` CaFixed

The return value of the transfer()/transferFrom() call is not checked. ```solidity=155 IERC20(token).transfer(to, amount); ```

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (30)

  • contracts/protocol/tokenization/base/ScaledBalanceTokenBaseERC721.sol
  • contracts/protocol/libraries/logic/SupplyLogic.sol
  • contracts/protocol/libraries/math/WadRayMath.sol
  • contracts/protocol/tokenization/NToken.sol
  • contracts/protocol/libraries/logic/PoolLogic.sol
  • contracts/protocol/libraries/configuration/UserConfiguration.sol
  • contracts/protocol/tokenization/NTokenUniswapV3.sol
  • contracts/protocol/libraries/types/DataTypes.sol
  • contracts/protocol/libraries/logic/ValidationLogic.sol
  • contracts/protocol/tokenization/NTokenMoonBirds.sol
  • contracts/protocol/configuration/PriceOracleSentinel.sol
  • contracts/protocol/pool/PoolConfigurator.sol
  • contracts/protocol/configuration/PoolAddressesProvider.sol
  • contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol
  • contracts/protocol/pool/Pool.sol
  • contracts/protocol/configuration/ACLManager.sol
  • contracts/protocol/libraries/helpers/Errors.sol
  • contracts/protocol/libraries/logic/ConfiguratorLogic.sol
  • contracts/protocol/pool/PoolStorage.sol
  • contracts/protocol/libraries/logic/LiquidationLogic.sol
  • contracts/protocol/libraries/configuration/ReserveConfiguration.sol
  • contracts/protocol/libraries/math/MathUtils.sol
  • contracts/protocol/libraries/logic/ReserveLogic.sol
  • contracts/protocol/libraries/logic/BorrowLogic.sol
  • contracts/protocol/libraries/types/ConfiguratorInputTypes.sol
  • contracts/protocol/configuration/PoolAddressesProviderRegistry.sol
  • contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol
  • contracts/protocol/libraries/logic/GenericLogic.sol
  • contracts/protocol/libraries/logic/MarketplaceLogic.sol
  • contracts/protocol/libraries/math/PercentageMath.sol

Part of ParaSpace Lending V1’s security record

ParaSpace Lending V1
Audits2 audits by 1 firmLast audit Dec 2022, by CertiK.
Critical findingsNo open critical findingsAcross the 2 reports, 2 critical findings were reported. Each is marked fixed by the firm that reported it.
Open ParaSpace Lending V1’s security record

Security firm

CertiK

1,044 projects reviewed · 1,375 reports · latest May 2025

See CertiK’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us