As of 3 Nov 2026, ParaSpace Lending V1 has 2 audit reports by 1 security firm on Trustblock; the latest is by CertiK, dated 23 Dec 2022. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Nov 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| ParaSpace (Audit #3) | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| ParaSpace - NFT Money Market | Security firm: CertiK | Date: | Scope: Files named in the report | 2 critical findings, fixed | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 2 |
| High | 0 | 2 | 2 |
| Medium | 1 | 0 | 4 |
| Low | 17 | 0 | 10 |
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. *** ## PoolAddressesProvider.sol ### External call(s) ```solidity=85 address oldImplementationAddress = _getProxyImplementation(id); ``` * This function call executes the following external call(s). * In `PoolAddressesProvider._getProxyImplementation`, * `InitializableImmutableAdminUpgradeabilityProxy(payableProxyAddress).implementation()` ```solidity=86 _updateImpl(id, newImplementationAddress); ``` * This function call executes the following external call(s). * In `PoolAddressesProvider._updateImpl`, * `proxy.initialize(newAddress,params)` * In `PoolAddressesProvider._updateImpl`, * `proxy.upgradeToAndCall(newAddress,params)` ### State variables written after the call(s) ```solidity=86 _updateImpl(id, newImplementationAddress); ``` * This function call executes the following assignment(s). * In `PoolAddressesProvider._updateImpl`, * `_addresses[id] = proxyAddress = address(proxy)` --- ## PoolAddressesProvider.sol ### External call(s) ```solidity=102 address oldPoolImpl = _getProxyImplementation(POOL); ``` * This function call executes the following external call(s). * In `PoolAddressesProvider._getProxyImplementation`, * `InitializableImmutableAdminUpgradeabilityProxy(payableProxyAddress).implementation()` ```solidity=103 _updateImpl(POOL, newPoolImpl); ``` * This function call executes the following external call(s). * In `PoolAddressesProvider._updateImpl`, * `proxy.initialize(newAddress,params)` * In `PoolAddressesProvider._updateImpl`, * `proxy.upgradeToAndCall(newAddress,params)` ### State variables written after the call(s) ```solidity=103 _updateImpl(POOL, newPoolImpl); ``` * This function call executes the following assignment(s). * In `PoolAddressesProvider._updateImpl`, * `_addresses[id] = proxyAddress = address(proxy)` --- ## PoolAddressesProvider.sol ### External call(s) ```solidity=118 address oldPoolConfiguratorImpl = _getProxyImplementation( POOL_CONFIGURATOR ); ``` * This function call executes the following external call(s). * In `PoolAddressesProvider._getProxyImplementation`, * `InitializableImmutableAdminUpgradeabilityProxy(payableProxyAddress).implementation()` ```solidity=121 _updateImpl(POOL_CONFIGURATOR, newPoolConfiguratorImpl); ``` * This function call executes the following external call(s). * In `PoolAddressesProvider._updateImpl`, * `proxy.initialize(newAddress,params)` * In `PoolAddressesProvider._updateImpl`, * `proxy.upgradeToAndCall(newAddress,params)` ### State variables written after the call(s) ```solidity=121 _updateImpl(POOL_CONFIGURATOR, newPoolConfiguratorImpl); ``` * This function call executes the following assignment(s). * In `PoolAddressesProvider._updateImpl`, * `_addresses[id] = proxyAddress = address(proxy)` --- ## BorrowLogic.sol ### External call(s) ```solidity=99 ( isFirstBorrowing, reserveCache.nextTotalStableDebt, reserveCache.nextAvgStableBorrowRate ) = IStableDebtToken(reserveCache.stableDebtTokenAddress).mint( params.user, params.onBehalfOf, params.amount, currentStableRate ); ``` ```solidity=110 ( isFirstBorrowing, reserveCache.nextScaledVariableDebt ) = IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint( params.user, params.onBehalfOf, params.amount, reserveCache.nextVariableBorrowIndex ); ``` ### State variables written after the call(s) ```solidity=125 reserve.updateInterestRates( reserveCache, params.asset, 0, params.releaseUnderlying ? params.amount : 0 ); ``` * This function call executes the following assignment(s). * In `ReserveLogic.updateInterestRates`, * `reserve.currentLiquidityRate = vars.nextLiquidityRate.toUint128()` * In `ReserveLogic.updateInterestRates`, * `reserve.currentStableBorrowRate = vars.nextStableRate.toUint128()` * In `ReserveLogic.updateInterestRates`, * `reserve.currentVariableBorrowRate = vars.nextVariableRate.toUint128()` ```solidity=125 reserve.updateInterestRates( reserveCache, params.asset, 0, params.releaseUnderlying ? params.amount : 0 ); ``` * This function call executes the following assignment(s). * In `ReserveLogic.updateInterestRates`, * `reserve.currentLiquidityRate = vars.nextLiquidityRate.toUint128()` * In `ReserveLogic.updateInterestRates`, * `reserve.currentStableBorrowRate = vars.nextStableRate.toUint128()` * In `ReserveLogic.updateInterestRates`, * `reserve.currentVariableBorrowRate = va
Open the report`validateEndAuction()` requires `isAuctioned(tokenId)`, however, doesn't require ```solidity require(collateralReserve.auctionStrategyAddress != address(0), Errors.AUCTION_NOT_ENABLED); ``` Theoretically, it can be turned off via a call to `PoolConfigurator.setReserveAuctionStrategyAddress()`.
Open the reportThe below `require` statement is invalid because the function caller can input the same address as the contract **POOL** to bypass the validation. Hence anyone can call the function `initialize()` to initialize the contract **NToken**. ```solidity=60 require(initializingPool == POOL, Errors.POOL_ADDRESSES_DO_NOT_MATCH); ```
Open the reportIn the function `getTokenPrice()`, the price from the ChainLink is used to calculate the amount0 and amount1 of Token0 and Token1. If the current price of ChainLink is out of the range of the position's lower and upper price, the amount0 or amount1 may be zero, which affects the price of the LP token finally. ```solidity=137 function getTokenPrice(uint256 tokenId) public view returns (uint256) { UinswapV3PositionData memory positionData = getOnchainPositionData( tokenId ); PairOracleData memory oracleData = _getOracleData(positionData);//ChainLink (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); } ``` **Proof of concept** - Case 1: Liquidate Asset 1. A liquidator can call the function `PoolCore.liquidateERC721()` to liquidate a borrower's non-healthy asset with Health Factor below 1. 2. Before the liquidation, the function `calculateUserAccountData()` calculates the total `xToken` balance of the user in the based currency used by the price oracle function `IPriceOracleGetter(oracle).getTokenPrice()`. 3. If the price obtained from ChainLink is at the left of `positionData.tickLower`, the number of token1 obtained will be 0. The price of the LP obtained according to the L158-L161 calculation logic may be lower than the price of the LP itself, which will eventually reduce the health factor of the asset. 4. In this case, the liquidator may successfully liquidate the asset that should be healthy. - Case 2: Borrow Asset 1. A user who provided collateral can call the function `PoolCore.borrow()` to borrow the ERC20 asset. 2. Before the borrowing, the function `calculateUserAccountData()` calculates the total `xToken` balance of the user in the based currency used by the price oracle function `IPriceOracleGetter(oracle).getTokenPrice()`. 3. If the price obtained from ChainLink is within the range of `positionData.tickLower` and `positionData.tickUpper`, but `currentTick` is exactly equal to `positionData.tickLower`(the number of token1 is 0), according to the calculation logic of L158-L161 the LP price may be higher than the actual price of LP, which will eventually increase the health factor of the asset. 4. In this case, the user may successfully borrow more ERC20 assets than the collateral.
Open the reportThe function `getReserveData()` is used to get the reserve data, however, there is no logical distinction between ERC20 and ERC721 assets. ```solidity=189 { DataTypes.ReserveData memory reserve = IPool( ADDRESSES_PROVIDER.getPool() ).getReserveData(asset); return ( reserve.accruedToTreasury, IERC20Detailed(reserve.xTokenAddress).totalSupply(), IERC20Detailed(reserve.variableDebtTokenAddress).totalSupply(), reserve.currentLiquidityRate, reserve.currentVariableBorrowRate, reserve.liquidityIndex, reserve.variableBorrowIndex, reserve.lastUpdateTimestamp ); } ```
Open the reportFlash loans are a way to borrow large amounts of money for a certain fee. The requirement is that the loans need to be returned within the same transaction in a block. If not, the transaction will be reverted. An attacker can use the borrowed money as the initial funds for an exploit to enlarge the profit and/or manipulate the token price in the decentralized exchanges. We find that the function `getOnchainPositionData()` relies on price calculations based on the current sqrt price, meaning that they would be susceptible to flash-loan attacks by manipulating the price of given pairs to the attacker's benefit. ```solidity=71 (uint160 currentPrice, int24 currentTick, , , , , ) = pool.slot0(); ``` The result of this function could impact the results of the below functions: - getLiquidityAmount() - getLiquidityAmountFromPositionData() - getLpFeeAmount() - getLpFeeAmountFromPositionData() Two functions among them are used in the `UiPoolDataProvider.sol`. - getLiquidityAmountFromPositionData() - getLpFeeAmountFromPositionData() Although the current usage of the abovementioned functions is only on UI, we would like to remind the client to be cautious about the usage of these functions.
Open the reportThe linked codes do not affect the functionality of the codebase and appear to be either remnant of test code or older functionality.
Open the reportThe function `_safeTransfer()` does not invoke {IERC721Receiver-onERC721Received} on a target address, which is against the EIP-721 standard. ```solidity=320 function _safeTransfer( address from, address to, uint256 tokenId, bytes memory ) internal virtual { _transfer(from, to, tokenId); } ```
Open the reportThe contract is serving as the underlying entity to interact with third-party `UniswapV3`, `OpenSea`, `LooksRare`, `X2Y2`, `MoonBird`, `ChainLink`, `ApeCoinStaking`, `IEACAggregatorProxy`, `IAtomicPriceAggregator` and NFT Oracle protocols. The scope of the audit treats 3rd party entities as black boxes and assumes their functional correctness. However, in the real world, 3rd parties can be compromised and this may lead to lost or stolen assets. In addition, upgrades of 3rd parties can possibly create severe impacts, such as increasing fees of 3rd parties, migrating to new LP pools, etc. The `ParaSpace` protocol allows users to borrow assets using NFT as collateral. If NFT prices fluctuate significantly in the third-party markets, the Supplier's health factory may fluctuate as well. This is a potential risk to this protocol and to the Supplier.
Open the reportThe linked `transfer()`/`transferFrom()` invocations do not check the return value of the function call, which should yield `true` in the case of proper ERC-20 implementation.
Open the reportThe return value of an external call is not stored in a local or state variable.
Open the reportThe function `initialize()` is `public` and can be called by anyone as long as the contract is deployed.
Open the reportThe title comment for `NToken.sol` is for `PToken`, not `NToken`. --- The comments for `executeBorrow()` and `executeRepay()` isolated positions are mentioned, however, there are no isolated positions in the code.
Open the reportThe variables `auctionRecoveryHealthFactor`, `newReserveFactor`, `newBorrowCap`, and `newFee` do not have reasonable boundaries, so they can be given arbitrary values after deploying.
Open the reportIf multiple pools are deployed, there may be a risk of code reentrancy. The hacker can call the `withdrawERC721()` method of one of the Pools and use the hook `_checkOnERC721Received` method of the ERC721 receiver to reenter the methods(such as the method `liquidationERC721()`) of other Pools. Please check if multiple Pools are allowed to be deployed at the same time.
Open the reportThe contract is serving as the underlying entity to interact with third-party `UniswapV3`, `OpenSea`, `X2Y2`, `MoonBird`,and NFT Oracle protocols. The scope of the audit treats 3rd party entities as black boxes and assumes their functional correctness. However, in the real world, 3rd parties can be compromised and this may lead to lost or stolen assets. In addition, upgrades of 3rd parties can possibly create severe impacts, such as increasing fees of 3rd parties, migrating to new LP pools, etc. The `ParaSpace` protocol allows users to borrow assets using NFT as collateral. If NFT prices fluctuate significantly in the third-party markets, the Supplier's health factory may fluctuate as well. This is a potential risk to this protocol and to the Supplier.
Open the reportThe return value of an external call is not stored in a local or state variable. ```solidity=290 stableDebtToken.burn(user, stableDebt); ``` --- ```solidity=651 INToken(vars.collateralXToken).burn(params.user, msg.sender, tokenIds); ``` --- ```solidity=80 Address.functionDelegateCall( params.marketplace.adapter, abi.encodeWithSelector( IMarketplace.matchAskWithTakerBid.selector, params.marketplace.marketplace, params.payload, priceEth ) ); ``` --- ```solidity=134 Address.functionDelegateCall( params.marketplace.adapter, abi.encodeWithSelector( IMarketplace.matchBidWithTakerAsk.selector, params.marketplace.marketplace, params.payload ) ); ``` --- ```solidity=200 IERC20(token).approve(params.marketplace.operator, 0); ``` --- ```solidity=201 IERC20(token).approve(params.marketplace.operator, price); ``` --- ```solidity=353 MarketplaceLogic.executeBuyWithCredit( _reserves, _reservesList, _usersConfig[onBehalfOf], DataTypes.ExecuteMarketplaceParams({ marketplaceId: marketplaceId, payload: payload, credit: credit, ethLeft: msg.value, marketplace: marketplace, orderInfo: orderInfo, WETH: WETH, referralCode: referralCode, maxStableRateBorrowSizePercent: _maxStableRateBorrowSizePercent, reservesCount: _reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel() }) ); ``` --- ```solidity=205 Address.functionCall( airdropContract, airdropParams, Errors.CALL_AIRDROP_METHOD_FAILED ); ``` --- ```solidity=107 INonfungiblePositionManager(_underlyingAsset).decreaseLiquidity( params ); ``` --- ```solidity=614 try IERC721Receiver(to).onERC721Received( _msgSender(), from, tokenId, _data ) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert( "ERC721: transfer to non ERC721Receiver implementer" ); } else { assembly { revert(add(32, reason), mload(reason)) } } } ```
Open the reportOne or more state variables are never used in the codebase. Variable `BORROWABLE_IN_ISOLATION_MASK` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=22 uint256 internal constant BORROWABLE_IN_ISOLATION_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDFFFFFFFFFFFFFFF; // prettier-ignore ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `UNBACKED_MINT_CAP_MASK` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=28 uint256 internal constant UNBACKED_MINT_CAP_MASK = 0xFFFFFFFFFFF000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `DEBT_CEILING_MASK` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=29 uint256 internal constant DEBT_CEILING_MASK = 0xF0000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // prettier-ignore ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `BORROWABLE_IN_ISOLATION_START_BIT_POSITION` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=40 uint256 internal constant BORROWABLE_IN_ISOLATION_START_BIT_POSITION = 61; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `IS_DYNAMIC_CONFIGS_START_BIT_POSITION` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=48 uint256 internal constant IS_DYNAMIC_CONFIGS_START_BIT_POSITION = 168; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `UNBACKED_MINT_CAP_START_BIT_POSITION` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=49 uint256 internal constant UNBACKED_MINT_CAP_START_BIT_POSITION = 176; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `DEBT_CEILING_START_BIT_POSITION` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=50 uint256 internal constant DEBT_CEILING_START_BIT_POSITION = 212; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `MAX_VALID_EMODE_CATEGORY` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=60 uint256 internal constant MAX_VALID_EMODE_CATEGORY = 255; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `MAX_VALID_UNBACKED_MINT_CAP` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=61 uint256 internal constant MAX_VALID_UNBACKED_MINT_CAP = 68719476735; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `MAX_VALID_DEBT_CEILING` in `ReserveConfiguration` is never used in `ReserveConfiguration`. ```solidity=62 uint256 internal constant MAX_VALID_DEBT_CEILING = 1099511627775; ``` ```solidity=12 library ReserveConfiguration { ``` --- Variable `BASE_CURRENCY_DECIMALS` in `LiquidationLogic` is never used in `LiquidationLogic`. ```solidity=82 uint256 private constant BASE_CURRENCY_DECIMALS = 18; ``` ```solidity=34 library LiquidationLogic { ``` --- Variable `______gap` in `VersionedInitializable` is never used in `ATokenDebtToken`. ```solidity=78 uint256[50] private ______gap; ``` ```solidity=14 contract ATokenDebtToken is RebasingDebtToken { ``` --- Variable `_allowances` in `MintableIncentivizedERC721` is never used in `NTokenMoonBirds`. ```solidity=95 mapping(address => mapping(address => uint256)) private _allowances; ``` ```solidity=27 contract NTokenMoonBirds is NToken, IMoonBirdBase { ```
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="paraspace"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
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