ParaSpace contest - csanuragjain's results

The First Ever Cross-Margin NFT Financialization Protocol.

General Information

Platform: Code4rena

Start Date: 28/11/2022

Pot Size: $192,500 USDC

Total HM: 33

Participants: 106

Period: 11 days

Judge: LSDan

Total Solo HM: 15

Id: 186

League: ETH

ParaSpace

Findings Distribution

Researcher Performance

Rank: 6/106

Findings: 9

Award: $5,428.52

QA:
grade-b

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-79

Awards

286.3766 USDC - $286.38

External Links

Lines of code

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

Vulnerability details

Impact

While removing any feeder the index is not updated. This incorrection in indexing will lead to _removeFeeder fail everytime as shown in POC. This could lead to problem when owner wants to remove a malicious feeder and would be unable to.

Proof of Concept

  1. Lets say owner has added 2 feeders as shown below:
feeders = {A,B} feederPositionMap[A].index = 0 feederPositionMap[B].index = 1
  1. Now owner removes feeder A by calling the removeFeeder function
function removeFeeder(address _feeder) external onlyWhenFeederExisted(_feeder) { _removeFeeder(_feeder); }
  1. This internally calls _removeFeeder function
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); }
  1. In our case below is state of variables:
uint8 feederIndex = feederPositionMap[_feeder].index; = feederPositionMap[A].index = 0 if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { // true since feederIndex is 0 feeders[feederIndex] = feeders[feeders.length - 1]; = feeders[2-1] = feeders[1] = B // thus feeders[0] = B feeders.pop(); // This deletes the last feeder, so feeders becomes {B} } delete feederPositionMap[_feeder]; // delete feederPositionMap[A]; revokeRole(UPDATER_ROLE, _feeder); emit FeederRemoved(_feeder);
  1. As shown in previous step, last feeder is removed and removed index is updated with last feeder. This makes:
feeders = {B} feederPositionMap[B].index = 1 // since this was never updated
  1. Owner decides to remove feeder B. Below is the code flow
uint8 feederIndex = feederPositionMap[_feeder].index; = feederPositionMap[B].index; = 1 if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { // fails due to overflow, feeders length is 1 so max index is 0 ... }
  1. As we can see this fails due to array index overflow since it tries to delete feeder from index 1 even though after deletion of A, feeder max index could be 0

Update the index as shown below:

function _removeFeeder(address _feeder) internal onlyWhenFeederExisted(_feeder) { uint8 feederIndex = feederPositionMap[_feeder].index; if (feederIndex >= 0 && feeders[feederIndex] == _feeder) { address feederLast = feeders[feeders.length - 1]; feeders[feederIndex] = feederLast; feeders.pop(); feederPositionMap[feederLast].index = feederIndex; } delete feederPositionMap[_feeder]; revokeRole(UPDATER_ROLE, _feeder); emit FeederRemoved(_feeder); }

#0 - c4-judge

2022-12-20T17:38:51Z

dmvt marked the issue as duplicate of #47

#1 - c4-judge

2023-01-09T15:35:16Z

dmvt changed the severity to 3 (High Risk)

#2 - c4-judge

2023-01-23T15:47:29Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: csanuragjain

Also found by: cccz, unforgiven

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
H-03

Awards

3171.1169 USDC - $3,171.12

External Links

Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/protocol/libraries/logic/LiquidationLogic.sol#L531

Vulnerability details

Impact

The debt tokens are being transferred before calculating the interest rates. But the interest rate calculation function assumes that debt token has not yet been sent thus the outcome currentLiquidityRate will be incorrect

Proof of Concept

  1. Liquidator L1 calls executeLiquidateERC20 for a position whose health factor <1
function executeLiquidateERC20( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, DataTypes.ExecuteLiquidateParams memory params ) external returns (uint256) { ... _burnDebtTokens(liquidationAssetReserve, params, vars); ... }
  1. This internally calls _burnDebtTokens
function _burnDebtTokens( DataTypes.ReserveData storage liquidationAssetReserve, DataTypes.ExecuteLiquidateParams memory params, ExecuteLiquidateLocalVars memory vars ) internal { ... // Transfers the debt asset being repaid to the xToken, where the liquidity is kept IERC20(params.liquidationAsset).safeTransferFrom( vars.payer, vars.liquidationAssetReserveCache.xTokenAddress, vars.actualLiquidationAmount ); ... // Update borrow & supply rate liquidationAssetReserve.updateInterestRates( vars.liquidationAssetReserveCache, params.liquidationAsset, vars.actualLiquidationAmount, 0 ); }
  1. Basically first it transfers the debt asset to xToken using below. This increases the balance of xTokenAddress for liquidationAsset
IERC20(params.liquidationAsset).safeTransferFrom( vars.payer, vars.liquidationAssetReserveCache.xTokenAddress, vars.actualLiquidationAmount );
  1. Now updateInterestRates function is called on ReserveLogic.sol#L169
function updateInterestRates( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache, address reserveAddress, uint256 liquidityAdded, uint256 liquidityTaken ) internal { ... ( vars.nextLiquidityRate, vars.nextVariableRate ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress) .calculateInterestRates( DataTypes.CalculateInterestRatesParams({ liquidityAdded: liquidityAdded, liquidityTaken: liquidityTaken, totalVariableDebt: vars.totalVariableDebt, reserveFactor: reserveCache.reserveFactor, reserve: reserveAddress, xToken: reserveCache.xTokenAddress }) ); ... }
  1. Finally call to calculateInterestRates function on DefaultReserveInterestRateStrategy#L127 contract is made which calculates the interest rate
function calculateInterestRates( DataTypes.CalculateInterestRatesParams calldata params ) external view override returns (uint256, uint256) { ... if (vars.totalDebt != 0) { vars.availableLiquidity = IToken(params.reserve).balanceOf(params.xToken) + params.liquidityAdded - params.liquidityTaken; vars.availableLiquidityPlusDebt = vars.availableLiquidity + vars.totalDebt; vars.borrowUsageRatio = vars.totalDebt.rayDiv( vars.availableLiquidityPlusDebt ); vars.supplyUsageRatio = vars.totalDebt.rayDiv( vars.availableLiquidityPlusDebt ); } ... vars.currentLiquidityRate = vars .currentVariableBorrowRate .rayMul(vars.supplyUsageRatio) .percentMul( PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor ); return (vars.currentLiquidityRate, vars.currentVariableBorrowRate); }
  1. As we can see in above code, vars.availableLiquidity is calculated as IToken(params.reserve).balanceOf(params.xToken) +params.liquidityAdded - params.liquidityTaken

  2. But the problem is that debt token is already transferred to xToken which means xToken already consist of params.liquidityAdded. Hence the calculation ultimately becomes (xTokenBeforeBalance+params.liquidityAdded) +params.liquidityAdded - params.liquidityTaken

  3. This is incorrect and would lead to higher vars.availableLiquidity which ultimately impacts the currentLiquidityRate

Transfer the debt asset post interest calculation

function _burnDebtTokens( DataTypes.ReserveData storage liquidationAssetReserve, DataTypes.ExecuteLiquidateParams memory params, ExecuteLiquidateLocalVars memory vars ) internal { IPToken(vars.liquidationAssetReserveCache.xTokenAddress) .handleRepayment(params.liquidator, vars.actualLiquidationAmount); // Burn borrower's debt token vars .liquidationAssetReserveCache .nextScaledVariableDebt = IVariableDebtToken( vars.liquidationAssetReserveCache.variableDebtTokenAddress ).burn( params.borrower, vars.actualLiquidationAmount, vars.liquidationAssetReserveCache.nextVariableBorrowIndex ); liquidationAssetReserve.updateInterestRates( vars.liquidationAssetReserveCache, params.liquidationAsset, vars.actualLiquidationAmount, 0 ); IERC20(params.liquidationAsset).safeTransferFrom( vars.payer, vars.liquidationAssetReserveCache.xTokenAddress, vars.actualLiquidationAmount ); ... ... }

#0 - c4-judge

2022-12-20T14:14:40Z

dmvt marked the issue as primary issue

#1 - c4-judge

2023-01-09T13:34:41Z

dmvt marked the issue as selected for report

#2 - c4-judge

2023-01-23T20:14:57Z

dmvt marked the issue as satisfactory

Awards

44.934 USDC - $44.93

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-402

External Links

Lines of code

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

Vulnerability details

Impact

It was observed that removeFeeder can be called by any user as it is missing any ACL. This means any user could remove all the feeders and hence impact the oracle pricing system (Feeders wont be able to add prices if removed)

Proof of Concept

  1. Observe the removeFeeder function
/// @notice Allows owner to remove feeder. /// @param _feeder feeder to remove function removeFeeder(address _feeder) external onlyWhenFeederExisted(_feeder) { _removeFeeder(_feeder); }
  1. Notice as per comment only owner is allowed to remove the feed but there is no modifier to verify the same, meaning any user can call this function to remove any feeders

Add a modifier to check that caller is owner (Notice onlyWhenFeederExisted(_feeder) is not required since already called in _removeFeeder function)

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

#1 - c4-judge

2022-12-20T16:58:32Z

dmvt marked the issue as duplicate of #31

#2 - c4-judge

2022-12-20T17:04:41Z

dmvt marked the issue as selected for report

#3 - c4-judge

2023-01-09T14:10:18Z

dmvt changed the severity to 3 (High Risk)

#4 - c4-judge

2023-01-09T14:38:55Z

dmvt marked the issue as not selected for report

#5 - c4-judge

2023-01-23T16:01:56Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: csanuragjain

Also found by: Lambda, eierina, joestakey, unforgiven

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
M-03

Awards

462.3488 USDC - $462.35

External Links

Lines of code

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

Vulnerability details

Impact

The safeTransfer function Safely transfers tokenId token from from to to, checking first that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked. But seems like this safety check got missed in the _safeTransfer function leading to non secure ERC721 transfers

Proof of Concept

  1. User calls the safeTransferFrom function (Using NToken contract which implements MintableIncentivizedERC721 contract)
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) external virtual override nonReentrant { _safeTransferFrom(from, to, tokenId, _data); }
  1. This makes an internal call to _safeTransferFrom -> _safeTransfer -> _transfer
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) external virtual override nonReentrant { _safeTransferFrom(from, to, tokenId, _data); } function _safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) internal { require( _isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved" ); _safeTransfer(from, to, tokenId, _data); } function _safeTransfer( address from, address to, uint256 tokenId, bytes memory ) internal virtual { _transfer(from, to, tokenId); }
  1. Now lets see _transfer function
function _transfer( address from, address to, uint256 tokenId ) internal virtual { MintableERC721Logic.executeTransfer( _ERC721Data, POOL, ATOMIC_PRICING, from, to, tokenId ); }
  1. This is calling MintableERC721Logic.executeTransfer which simply transfers the asset

  2. In this full flow there is no check to see whether to address can support ERC721 which fails the purpose of safeTransferFrom function

  3. Also notice the comment mentions that data parameter passed in safeTransferFrom is sent to recipient in call but there is no such transfer of data

Add a call to onERC721Received for recipient and see if the recipient actually supports ERC721

#0 - c4-judge

2022-12-20T17:56:56Z

dmvt marked the issue as duplicate of #51

#1 - c4-judge

2022-12-20T17:58:09Z

dmvt marked the issue as selected for report

#2 - c4-judge

2023-01-23T16:16:21Z

dmvt marked the issue as satisfactory

#3 - C4-Staff

2023-02-01T19:10:09Z

captainmangoC4 marked the issue as selected for report

Findings Information

🌟 Selected for report: xiaoming90

Also found by: csanuragjain, unforgiven

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-284

Awards

731.7962 USDC - $731.80

External Links

Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/protocol/tokenization/libraries/ApeStakingLogic.sol#L53

Vulnerability details

Impact

The ApeStakingLogic library is not handling the withdraw correctly and will transfer user any existing balance held by the contract in addition to user requested amount.

Proof of Concept

  1. Assume initial NToken ApeCoin balance is 1000 at NTokenMAYC contract (Also true for NTokenBAYC)

  2. User call withdrawBAKC function at PoolApeStaking#L120

function withdrawBAKC( address nftAsset, ApeCoinStaking.PairNftWithAmount[] memory _nftPairs ) external nonReentrant { ... nTokenApeStaking.withdrawBAKC(_nftPairs, msg.sender); ... }
  1. This makes withdrawBAKC function call to NTokenMAYC contract (Assume passed Paired argument contained MAYC NFT's)
function withdrawBAKC( ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, address _apeRecipient ) external onlyPool nonReentrant { ApeStakingLogic.withdrawBAKC( _apeCoinStaking, POOL_ID(), _nftPairs, _apeRecipient ); }
  1. This uses library ApeStakingLogic function withdrawBAKC which is implemented as below:
function withdrawBAKC( ApeCoinStaking _apeCoinStaking, uint256 poolId, ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, address _apeRecipient ) external { ... if (poolId == BAYC_POOL_ID) { _apeCoinStaking.withdrawBAKC(_nftPairs, _otherPairs); } else { _apeCoinStaking.withdrawBAKC(_otherPairs, _nftPairs); } uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance); }
  1. This calls withdrawBAKC on the contract ApeCoinStaking
function withdrawBAKC(PairNftWithAmount[] calldata _baycPairs, PairNftWithAmount[] calldata _maycPairs) external { updatePool(BAKC_POOL_ID); _withdrawPairNft(BAYC_POOL_ID, _baycPairs); _withdrawPairNft(MAYC_POOL_ID, _maycPairs); } function _withdrawPairNft(uint256 mainTypePoolId, PairNftWithAmount[] calldata _nfts) private { ... _withdraw(BAKC_POOL_ID, position, amount, mainTokenOwner); ... } function _withdraw(uint256 _poolId, Position storage _position, uint256 _amount, address _recipient) private { ... apeCoin.safeTransfer(_recipient, _amount); }
  1. NTokenMAYC contract receives the withdrawn apeCoin say X amount. The final balance of contract becomes now 1000+X

  2. Finally the last logic at withdrawBAKC function at ApeStakingLogic executes

uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance);
  1. Since this is transferring all ape coin balance so full 1000+X balance is transferred to User even though only X amount should be transferred

Keep hold of initial apeCoin balance before calling _apeCoinStaking.withdrawBAKC. Subtract the final balance with this initial balance to get the correct amount to send

function withdrawBAKC( ApeCoinStaking _apeCoinStaking, uint256 poolId, ApeCoinStaking.PairNftWithAmount[] memory _nftPairs, address _apeRecipient ) external { ApeCoinStaking.PairNftWithAmount[] memory _otherPairs = new ApeCoinStaking.PairNftWithAmount[](0); uint256 initialBalance = _apeCoinStaking.apeCoin().balanceOf(address(this)); if (poolId == BAYC_POOL_ID) { _apeCoinStaking.withdrawBAKC(_nftPairs, _otherPairs); } else { _apeCoinStaking.withdrawBAKC(_otherPairs, _nftPairs); } uint256 balance = _apeCoinStaking.apeCoin().balanceOf(address(this)); _apeCoinStaking.apeCoin().safeTransfer(_apeRecipient, balance-initialBalance); }

#0 - c4-judge

2022-12-20T14:09:49Z

dmvt marked the issue as primary issue

#1 - c4-judge

2023-01-23T20:05:28Z

dmvt marked the issue as satisfactory

#2 - trust1995

2023-01-26T20:12:45Z

Contract is behaving as intended. NTokens should not hold ERC20 tokens such as APE. The premise stated in bullet point 1 is false. When ApeStakingLogic's withdrawBAKC is called, it is running in the context of an Ape NToken. Therefore, no harm is done, and whatever APEcoins we've managed to withdraw are safely sent to the recipient.

EDIT: After looking at the edge case scenario explained in dup report, I still believe protocol is working as intended. It is user's responsibility to withdraw APE yield before selling a part of a BAYC/MAYC pair. It is a known functionality of Ape staking that transfer of the Ape means transfer of underlying yield, and is usually priced in.

#3 - csanuragjain

2023-01-27T07:34:41Z

Actually NToken are holding APE coins.

Mainnet Example (1712 APE coin): NTokenBAYC - https://etherscan.io/address/0xdb5485C85Bd95f38f9def0cA85499eF67dC581c0

It has already applied before-after balance check and thus prevent excess withdrawal

#4 - dmvt

2023-01-27T10:15:24Z

It should also be noted that privately the sponsor confirmed this issue, although thinks it should be rated medium risk.

#5 - trust1995

2023-01-27T10:22:47Z

Would really like judge to relate to the argument laid in previous comment and explain the chosen severity in that perspective.

#6 - c4-judge

2023-01-27T10:39:42Z

dmvt changed the severity to 2 (Med Risk)

#7 - C4-Staff

2023-02-01T19:10:20Z

captainmangoC4 marked issue #284 as primary and marked this issue as a duplicate of 284

Findings Information

🌟 Selected for report: xiaoming90

Also found by: cccz, csanuragjain, imare, unforgiven

Labels

bug
2 (Med Risk)
satisfactory
duplicate-286

Awards

355.653 USDC - $355.65

External Links

Lines of code

https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/protocol/tokenization/NTokenMoonBirds.sol#L70

Vulnerability details

Impact

The current implementation of NTokenMoonBirds restricts it to receive any airdrop. This is because onERC721Received only allows MoonBird contract as sender of NFT which means any other sender is plainly rejected

Proof of Concept

  1. Airdrop NFT is sent to NTokenMoonBirds contract using say safeTransferFrom function
  2. Transfer function invokes the onERC721Received method on receiving contract which in this case is NTokenMoonBirds contract
  3. Airdrop gets rejected since sender is not MoonDrop contract
function onERC721Received( address operator, address from, uint256 id, bytes memory ) external virtual override returns (bytes4) { // only accept MoonBird tokens require(msg.sender == _underlyingAsset, Errors.OPERATION_NOT_SUPPORTED); ... }

The hardrcore requirement for sender can be removed and contract should allow airdrop NFT

function onERC721Received( address operator, address from, uint256 id, bytes memory ) external virtual override returns (bytes4) { // 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)) { return this.onERC721Received.selector; } // only accept MoonBird tokens if(msg.sender == _underlyingAsset){ // supply the received token to the pool and set it as collateral DataTypes.ERC721SupplyParams[] memory tokenData = new DataTypes.ERC721SupplyParams[](1); tokenData[0] = DataTypes.ERC721SupplyParams({ tokenId: id, useAsCollateral: true }); POOL.supplyERC721FromNToken(_underlyingAsset, tokenData, from); } return this.onERC721Received.selector; }

#0 - c4-judge

2022-12-20T14:14:02Z

dmvt marked the issue as primary issue

#1 - c4-judge

2023-01-23T20:10:57Z

dmvt marked the issue as satisfactory

#2 - C4-Staff

2023-02-01T19:10:30Z

captainmangoC4 marked issue #286 as primary and marked this issue as a duplicate of 286

Findings Information

🌟 Selected for report: Trust

Also found by: KingNFT, Lambda, csanuragjain, eierina, imare

Labels

2 (Med Risk)
satisfactory
duplicate-497

Awards

266.7397 USDC - $266.74

External Links

Judge has assessed an item in Issue #229 as M risk. The relevant finding follows:

Support for IERC165 interface id is missed Contract: https://github.com/code-423n4/2022-11-paraspace/blob/main/paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol#L572

Impact: Contract fails to support a valid interface which could lead to failure of genuine calls

Steps:

Observe the supportsInterface function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId; } Observe that support for IERC165 interface id is missing Recommendation: Kindly revise the function as below:

function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || interfaceId == type(IERC165).interfaceId; }

#0 - c4-judge

2023-01-25T15:44:23Z

dmvt marked the issue as duplicate of #52

#1 - c4-judge

2023-01-25T15:44:29Z

dmvt marked the issue as satisfactory

Valid Approved users will not be allowed to transfer

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

Impact: The transferFrom allows an approved user to transfer NFT on behalf of owner. But seems like this feature is broken since one of its internal call requires caller to be owner only which means the call would fail

Steps:

  1. Approved User calls the transferFrom function
function transferFrom( address from, address to, uint256 tokenId ) external virtual override nonReentrant { //solhint-disable-next-line max-line-length require( _isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved" ); _transfer(from, to, tokenId); }
  1. This makes call to _transfer function. Since this is an abstract contract which is implemented by NToken contract. Now NToken has overridden its own _transfer function
function _transfer( address from, address to, uint256 tokenId, bool validate ) internal virtual { address underlyingAsset = _underlyingAsset; uint256 fromBalanceBefore = collateralizedBalanceOf(from); uint256 toBalanceBefore = collateralizedBalanceOf(to); bool isUsedAsCollateral = _transferCollateralizable(from, to, tokenId); ... }
  1. This calls _transferCollateralizable which internally calls executeTransfer function
unction _transferCollateralizable( address from, address to, uint256 tokenId ) internal virtual returns (bool isUsedAsCollateral_) { isUsedAsCollateral_ = MintableERC721Logic .executeTransferCollateralizable( _ERC721Data, POOL, ATOMIC_PRICING, from, to, tokenId ); } function executeTransferCollateralizable( ... ) external returns (bool isUsedAsCollateral_) { ... executeTransfer(erc721Data, POOL, ATOMIC_PRICING, from, to, tokenId); }
  1. The executeTransfer function checks whether caller is owner itself. Since caller is an approved user and not owner so this call fails
function executeTransfer( MintableERC721Data storage erc721Data, IPool POOL, bool ATOMIC_PRICING, address from, address to, uint256 tokenId ) public { require( erc721Data.owners[tokenId] == from, "ERC721: transfer from incorrect owner" ); ... }

Recommendation: Either remove the owner requirement or b) if this is meant to be called only by owner then remove the approved user allow

Centralization Risk

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

Impact: It seems the poolAdmin holds too much power including changing reward controller, rescue tokens etc. This can allow poolAdmin to impact all users by changing the config or draining the contract. In this example we will see one example for setIncentivesController

Steps:

  1. PoolAdmin calls setIncentivesController and set rewardController to zero
  2. This causes Users will stop getting incentives on their stakes. So if User decides to burn then the reward incentives are gone permanently

Recommendation: Keep the poolAdmin as multiSig and behind timelock to prevent immediate changes

Asset is not removed properly

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

Impact: Asset are deleted directly leaving blanks in middle of assets. This could have unintended effects based on how assets is being utlized

Steps:

  1. Assume there were 2 assets {A,B}
  2. removeAsset function is called to remove asset A
  3. This internally calls _removeAsset function
function _removeAsset(address _asset) internal onlyWhenAssetExisted(_asset) { uint8 assetIndex = assetFeederMap[_asset].index; = assetFeederMap[A].index = 0 delete assets[assetIndex]; = delete assets[0]; delete assetPriceMap[_asset]; delete assetFeederMap[_asset]; emit AssetRemoved(_asset); }
  1. Since assets[0] is directly deleted and not swapped with last array value so assets would look like below:
assets[0] = {} assets[1] = {B}

Recommendation: Instead of directly deleting the asset, swap deleted asset index with last index asset and then pop last element. Sample example is _removeFeeder

Support for IERC165 interface id is missed

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

Impact: Contract fails to support a valid interface which could lead to failure of genuine calls

Steps:

  1. Observe the supportsInterface
function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId; }
  1. Observe that support for IERC165 interface id is missing

Recommendation: Kindly revise the function as below:

function supportsInterface(bytes4 interfaceId) external view virtual override(IERC165) returns (bool) { return interfaceId == type(IERC721Enumerable).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || interfaceId == type(IERC165).interfaceId; }

#0 - c4-judge

2023-01-25T15:44:33Z

dmvt marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter