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
Rank: 19/106
Findings: 7
Award: $1,303.37
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Englave
Also found by: 9svR6w, Jeiwan, Josiah, Lambda, RaymondFam, Trust, csanuragjain, kaliberpoziomka8552, minhquanym, unforgiven
286.3766 USDC - $286.38
In NFTFloorOracle
, the index of every feeder is stored in the mapping feederPositionMap
:
function _addFeeder(address _feeder) internal onlyWhenFeederNotExisted(_feeder) { feeders.push(_feeder); feederPositionMap[_feeder].index = uint8(feeders.length - 1); feederPositionMap[_feeder].registered = true;
When a feeder is removed using _removeFeeder
, the corresponding entry in the mapping is also deleted. However, the function _removeFeeder
also swaps the feeder with the last entryy in the feeders
array before pop
ping the last element. Therefore, the index of the element that is at the last position changes (unless the last element is deleted). However, the index (within feederPositionMap
) of the last entry is not updated and the next removal will therefore no longer remove the feeder from the feeders array (because of the check feeders[feederIndex] == _feeder
).
Therefore, the system will still use a feeder that should have been removed within _combine
(because the function iterates over the whole feeders
array).
Let's say we have three feeders [A, B, C, D] (i.e. with indices 0, 1, 2, 3 in the feederPositionMap
mapping) in feeders
. An owner now calls removeFeeder(B)
. Afterwards, feeders
is [A, C, D], but feederPositionMap[C]
is still 2. When removeFeeder(C)
is called, the feeder will not be removed and it will still be used. When removeFeeder(D)
is called, the transaction will even revert because of an out of bounds access (index 3).
Also update the index of the swapped entry.
#0 - c4-judge
2022-12-20T14:06:40Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-09T15:35:11Z
dmvt changed the severity to 3 (High Risk)
#2 - c4-judge
2023-01-23T15:46:54Z
dmvt marked the issue as satisfactory
#3 - C4-Staff
2023-02-01T19:09:46Z
captainmangoC4 marked issue #79 as primary and marked this issue as a duplicate of 79
π Selected for report: csanuragjain
Also found by: Lambda, eierina, joestakey, unforgiven
355.653 USDC - $355.65
MintableIncentivizedERC721.safeTransferFrom
calls _safeTransfer
, which then calls _transfer
. This function in turn calls MintableERC721Logic.executeTransfer
for the actual transfer. However, MintableERC721Logic.executeTransfer
only executes the transfer and does not call onERC721Received
when the recipient is a smart contract. Because of that, the function will also not be called when safeTransferFrom
is used. This is a violation of EIP 721 which states:
/// (...) . When transfer is complete, this function /// checks if `_to` is a smart contract (code size > 0). If so, it calls /// `onERC721Received` on `_to` and throws if the return value is not /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
Let's say that a MintableIncentivizedERC721
NFT is transferred to a smart contract that does not want to receive NFTs (for instance, because it does not have functionality to recover them). This contract therefore implements an onERC721Received
function that rejects the transfer. However, even when using safeTransferFrom
, this will be ignored and the transfer will still succeed, although it should not according to EIP 721.
Call onERC721Received
when the recipient is a smart contract.
#0 - c4-judge
2022-12-20T14:07:13Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-23T16:16:09Z
dmvt marked the issue as satisfactory
#2 - WalidOfNow
2023-01-25T18:41:41Z
This is by design. we want to avoid re-entrancy to our contracts and so we removed calling the hook
#3 - C4-Staff
2023-02-01T19:10:02Z
captainmangoC4 marked issue #235 as primary and marked this issue as a duplicate of 235
π Selected for report: ladboy233
Also found by: Kong, Lambda, R2, __141345__, mahdikarimi
266.7397 USDC - $266.74
https://github.com/code-423n4/2022-11-paraspace/blob/154c657d927c66fc8a45a9a6df881ec937323be8/paraspace-core/contracts/misc/UniswapV3OracleWrapper.sol#L74 https://github.com/code-423n4/2022-11-paraspace/blob/c01a980e5d6e15b2993b912c3569ed8b5236ff33/paraspace-core/contracts/misc/ParaSpaceFallbackOracle.sol#L56
The oracles UniswapV3OracleWrapper
and ParaSpaceFallabackOracle
directly query the Uniswap spot price / reserves to determine the asset price. In UniswapV3OracleWrapper
, this is done in the following code:
(uint160 currentPrice, int24 currentTick, , , , , ) = pool.slot0();
Similarly, ParaSpaceFallbackOracle
directly calls getReserves
on the Uniswap pair and passes this value to getAmountOut
(to calculate the asset price):
(uint256 left, uint256 right, ) = pair.getReserves(); (uint256 tokenReserves, uint256 ethReserves) = (asset < WETH) ? (left, right) : (right, left); uint8 decimals = ERC20(asset).decimals(); //returns price in 18 decimals return IUniswapV2Router01(UNISWAP_ROUTER).getAmountOut( //@audit Flashloan attack? 10**decimals, tokenReserves, ethReserves );
This can be exploited by an attacker to manipulate the asset price temporarily with a flash loan. In the context of ParaSpace, two kind of attacks are possible: 1.) Undercollaterized loans: Here, an attacker temporarily inflates the asset price with a flash loan to take out a loan that would not be allowed with the real asset price (because the health factor would be too low). If done properly, this can lead to situations where the loans are undercollaterized (if the real prices would be used). 2.) Liquidating positions: An attacker can also temporarily bring the price of an asset down to liquidate a position that is healthy (if the real price is used).
Use a TWAP. See for instance https://medium.com/blockchain-development-notes/a-guide-on-uniswap-v3-twap-oracle-2aa74a4a97c5 for details.
#0 - c4-sponsor
2022-12-06T04:05:49Z
yubo-ruan marked the issue as disagree with severity
#1 - c4-sponsor
2022-12-06T04:05:52Z
yubo-ruan marked the issue as sponsor disputed
#2 - c4-judge
2022-12-20T14:07:00Z
dmvt marked the issue as primary issue
#3 - c4-judge
2023-01-09T16:42:57Z
dmvt changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-01-23T16:14:25Z
dmvt marked the issue as satisfactory
#5 - C4-Staff
2023-02-01T19:10:12Z
captainmangoC4 marked issue #242 as primary and marked this issue as a duplicate of 242
π Selected for report: IllIllI
Also found by: 0x52, 0xNazgul, Franfran, IllIllI, Jeiwan, Lambda, RaymondFam, Rolezn, Trust, __141345__, codecustard, erictee, gzeon, hansfriese, imare, rbserver, rvierdiiev, seyni, skinz, ujamal_
18.3064 USDC - $18.31
ParaSpaceOracle.getAssetPrice
uses the result of latestAnswer()
directly. This function is deprecated (see for instance the comment on Etherscan) and it is strongly recommended to use latestRoundData()
instead. The problem with the current usage in ParaSpace is that the age of the answer is not checked. The answer can be arbitrarily old, but it will still be used as the price of the asset. This can lead to wrong valuations and allow borrows that should not be allowed (because of the health factor).
Let's say the price of an asset decreased signficantly (e.g., 50%) over multiple days, but the chainlink feed was not updated for some reason. ParaSpace will still use the old price, which opens up an arbitrage possibility (buying on an exchange cheaply, taking out a loan and never paying it back) that could ultimately kill the protocol.
Use latestRoundData()
which provides the timestamp. If the answer is too old, use the fallback oracle.
#0 - c4-judge
2022-12-20T17:44:24Z
dmvt marked the issue as duplicate of #5
#1 - c4-judge
2023-01-23T15:51:40Z
dmvt marked the issue as satisfactory
266.7397 USDC - $266.74
MintableIncentivizedERC721
intends to support EIP165. It inherits from IERC165
and implements a supportsInterface
function. However, this function is not compliant with EIP 165, which states
Therefore the implementing contract will have a supportsInterface function that returns: true when interfaceID is 0x01ffc9a7 (EIP165 interface) false when interfaceID is 0xffffffff true for any other interfaceID this contract implements false for any other interfaceID
In contrast to the specification, the supportsInterface
of MintableIncentivizedERC721
does not return true for the interface ID 0x01ffc9a7.
Because of that, the EIP 165 functionality is currently almost useless. Because according to EIP 165, smart contracts should first always perform a contract.supportsInterface(0x01ffc9a7)
call and only then check if the desired interfaces are supported.
Note that this also makes the tokens not compliant with EIP 721. According to EIP 721, "Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces" and clients can therefore rely on this to detect ERC721 tokens.
A NFT marketplace wants to support ERC721 tokens only and does not allow selling any other assets. A way to check this that according to EIP 721 & EIP 165 (in fact the only way to check this) is the following code:
require(contract.supportsInterface(0x01ffc9a7) && contract.supportsInterface(0x80ac58cd));
However, this code will fail with MintableIncentivizedERC721
, meaning they cannot be sold.
Additionally call super.supportsInterface(interfaceId)
.
#0 - c4-judge
2022-12-20T14:07:21Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-23T16:16:53Z
dmvt marked the issue as satisfactory
#2 - C4-Staff
2023-02-01T19:11:29Z
captainmangoC4 marked issue #497 as primary and marked this issue as a duplicate of 497
π Selected for report: IllIllI
Also found by: 0x4non, 0x52, 0xAgro, 0xNazgul, 0xSmartContract, 0xackermann, 9svR6w, Awesome, Aymen0909, B2, BRONZEDISC, Bnke0x0, Deekshith99, Deivitto, Diana, Dravee, HE1M, Jeiwan, Kaiziron, KingNFT, Lambda, Mukund, PaludoX0, RaymondFam, Rolezn, Sathish9098, Secureverse, SmartSek, __141345__, ahmedov, ayeslick, brgltd, cccz, ch0bu, chrisdior4, cryptonue, cryptostellar5, csanuragjain, datapunk, delfin454000, erictee, gz627, gzeon, helios, i_got_hacked, ignacio, imare, jadezti, jayphbee, joestakey, kankodu, ksk2345, ladboy233, martin, nadin, nicobevi, oyc_109, pashov, pavankv, pedr02b2, pzeus, rbserver, ronnyx2017, rvierdiiev, shark, unforgiven, xiaoming90, yjrwkk
103.9175 USDC - $103.92
According to a comment in ParaSpaceOracle
, a fallback oracle should be used when Chainlink returns negative prices:
* - If the returned price by a Chainlink aggregator is <= 0, the call is forwarded to a fallback oracle
However, this is currently not the case. The answer is first cast to a uint (from an int):
price = uint256(source.latestAnswer());
The fallback is then only used if the casted answer is 0. However, if source.latestAnswer()
was smaller than 0, price
will be a huge number (because of the casting) that will be wrongly used.
Let's say source.latestAnswer() = -1
. Then, price
will be equal to type(uint256).max
(i.e., a huge number) and this wrong price will be used by the system, leading to completely wrong valuations.
Check that the latest answer is <= 0 before casting, use the fallback oracle in that case.
#0 - c4-judge
2022-12-20T14:06:49Z
dmvt marked the issue as primary issue
#1 - c4-judge
2023-01-09T22:54:07Z
dmvt changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-01-25T10:57:09Z
dmvt marked the issue as grade-b