Platform: Code4rena
Start Date: 07/04/2022
Pot Size: $100,000 USDC
Total HM: 20
Participants: 62
Period: 7 days
Judge: LSDan
Total Solo HM: 11
Id: 107
League: ETH
Rank: 36/62
Findings: 2
Award: $233.86
🌟 Selected for report: 0
🚀 Solo Findings: 0
25.7805 USDC - $25.78
_normalizeAggregatorAnswer
Likelihood low, impact high.
The Chainlink latestAnswer
function included in IAggregatorV3Interface
and called in NFTVault#_normalizeAggregatorAnswer()
is considered deprecated and no longer included in the Chainlink API documentation.
It's considered best practice to use the latestRoundData
function instead. (API docs). latestAnswer
returns only the value of the latest price, whereas latestRoundData
returns additional information that can be used to validate whether a price is stale:
int256 answer = oracle.latestAnswer(); uint8 decimals = oracle.decimals();
Example using latestRoundData
:
(uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = oracle.latestRoundData(); require(answeredInRound >= roundId, "Stale data"); require(timestamp != 0, "Zero timestamp"); uint8 decimals = oracle.decimals();
Stale prices may impact collateral value and credit limit calculations, incorrectly reporting a position as under- or overcollateralized.
See also OpenZeppelin's guidelines for safely using latestRoundData
and latestAnswer
, and consider the impact of a stale or reverting price feed.
#0 - spaghettieth
2022-04-14T12:59:40Z
Duplicate of #4
🌟 Selected for report: Dravee
Also found by: 0x1f8b, 0xDjango, 0xkatana, AuditsAreUS, Cityscape, Foundation, Funen, Hawkeye, IllIllI, JC, JMukesh, Jujic, Kthere, PPrieditis, Picodes, Ruhum, TerrierLover, TrungOre, WatchPug, berndartmueller, catchup, cccz, cmichel, delfin454000, dy, ellahi, hickuphh3, horsefacts, hubble, hyh, ilan, jayjonah8, kebabsec, kenta, minhquanym, pauliax, rayn, reassor, rfa, robee, samruna
208.077 USDC - $208.08
Throughout the codebase, most state changing functions that update protocol parameters do not emit events.
For example:
NFTVault#setCreditLimitRate
NFTVault#setLiquidationLimitRate
NFTVault#toggleFallbackOracle
NFTVault#setBorrowAmountCap
NFTVault#setDebtInterestApr
NFTVault#setValueIncreaseLockRate
NFTVault#setJPEGLockTime
NFTVault#disableFloorOverride
NFTVault#setOrganizationFeeRate
NFTVault#setInsurancePurchaseRate
NFTVault#setInsuranceLiquidationPenaltyRate
This limits the ability for off-chain monitoring tools to observe and react to changes to protocol parameters. It's considered a best practice to emit events for state changing operations, especially changes to key protocol parameters.
ETH transfers in FungibleAssetVaultForDAO#withdraw
are performed using payable(msg.sender).transfer
.
It's considered a best practice to avoid this pattern for ETH transfers, since it forwards a fixed amount of gas and may revert if future gas costs change. (See the Consensys Diligence article here.)
Since the withdraw
function is already nonReentrant
, consider using OpenZeppelin's Address.sendValue
instead.
_massUpdatePools
The _massUpdatePools
function in LPFarming.sol
iterates over all active and inactive LP pools. The contract owner may add, but not update or remove pools.
If the poolInfo
array grows too large, add
, set
, and newEpoch
may revert, impacting the ability to create and manage pools and preventing creation of new rewards epochs.
Additionally, the _updatePool
function contains an external call to pool.lpToken.balanceOf
. If a malicious or malfunctioning LP token is added to a configured pool, this call may revert.
Likelihood is mitigated since adding pools is a permissioned function and there will likely be a limited number of LP pools. Owner should monitor gas usage when adding a new pool, and take care to validate approved LP token addresses. Consider adding the ability to disable, remove, or reconfigure a pool.
function _massUpdatePools() internal { uint256 length = poolInfo.length; for (uint256 pid = 0; pid < length; ++pid) { _updatePool(pid); } }
function _updatePool(uint256 _pid) internal { PoolInfo storage pool = poolInfo[_pid]; if (pool.allocPoint == 0) { return; } uint256 blockNumber = _blockNumber(); //normalizing the pool's `lastRewardBlock` ensures that no rewards are distributed by staking outside of an epoch uint256 lastRewardBlock = _normalizeBlockNumber(pool.lastRewardBlock); if (blockNumber <= lastRewardBlock) { return; } uint256 lpSupply = pool.lpToken.balanceOf(address(this)); if (lpSupply == 0) { pool.lastRewardBlock = blockNumber; return; } uint256 reward = ((blockNumber - lastRewardBlock) * epoch.rewardPerBlock * 1e36 * pool.allocPoint) / totalAllocPoint; pool.accRewardPerShare = pool.accRewardPerShare + reward / lpSupply; pool.lastRewardBlock = blockNumber; }
claimAll()
The claimAll
function in LPFarming.sol
iterates over all active and inactive LP pools in the poolInfo
array to claim rewards. The contract owner may add, but not remove pools.
If the poolInfo
array grows too large, claimAll
, may revert, preventing users from claiming accrued rewards. Additionally, the _updatePool
function contains an external call to pool.lpToken.balanceOf
that could revert if a malicious or malfunctioning LP token is added to pool configuration.
Severity is significantly mitigated since the user may still claim rewards from a single pool using claim(uint256)
. Likelihood is mitigated since adding pools is a permissioned function and there will likely be a limited number of LP pools. Owner should monitor gas usage when adding new pools.
function claimAll() external nonReentrant noContract(msg.sender) { for (uint256 i = 0; i < poolInfo.length; i++) { _updatePool(i); _withdrawReward(i); } uint256 rewards = userRewards[msg.sender]; require(rewards > 0, "no_reward"); jpeg.safeTransfer(msg.sender, rewards); userRewards[msg.sender] = 0; emit ClaimAll(msg.sender, rewards); }
noContract
modifierThe noContract
modifier, whitelistedContracts
mapping, and setContractWhitelisted
functions are duplicated across multiple contracts. Consider extracting this functionality to a shared abstract contract.
Usages:
The Ownable()
and ReentrancyGuard()
calls included inline in the JPEGLock
constructor may be omitted since these base contracts require no constructor arguments.
constructor(IERC20 _jpeg) Ownable() ReentrancyGuard() { jpeg = _jpeg; lockTime = 365 days; }
Suggested:
constructor(IERC20 _jpeg) { jpeg = _jpeg; lockTime = 365 days; }
The local variable initializer
inside the category initialization loop in NFTVault#constructor
shadows the initializer
modifier. Consider renaming this local variable.
for (uint256 i = 0; i < _typeInitializers.length; i++) { // QA: variable name shadows `initializer` modifier NFTCategoryInitializer memory initializer = _typeInitializers[i]; nftTypeValueETH[initializer.hash] = initializer.valueETH; for (uint256 j = 0; j < initializer.nfts.length; j++) { nftTypes[initializer.nfts[j]] = initializer.hash; } }
Natspec documentation is missing for the _jpeg
parameter in Controller#constructor
.
Since approvedStrategies[_token][_strategy]
returns a boolean value, you can safely omit the == true
check on line 87 of Controller#setStrategy
.