Platform: Code4rena
Start Date: 23/02/2024
Pot Size: $92,000 USDC
Total HM: 0
Participants: 47
Period: 10 days
Judge: 0xTheC0der
Id: 336
League: ETH
Rank: 31/47
Findings: 1
Award: $694.30
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: CodeWasp
Also found by: 0xdice91, 0xlemon, Aamir, Al-Qa-qa, AlexCzm, BAHOZ, Bauchibred, Breeje, DadeKuma, Fassi_Security, PetarTolev, Shield, SpicyMeatball, Trust, ZanyBonzy, cheatc0d3, gesha17, haxatron, imare, jesjupyter, kutugu, lsaudit, marchev, merlinboii, nnez, osmanozdemir1, peanuts, radev_sw, twicek, visualbits
694.2987 USDC - $694.30
ID | Title | Severity |
---|---|---|
1 | Inconsistent Admin Setting Logic | Low |
2 | Admin Role for Uniswap Governance is not ensured | Low |
3 | Incorrect Comment in UniStaker.sol#_withdraw() function | Low |
4 | Allow rewards duration to can be changed | Low |
5 | Change some mappings to named mappings to follow protocol mappings convention | NC |
6 | Unnecessary else Block | NC |
7 | Redundant IERC20 Import | NC |
8 | Constants in Comparisons | NC |
9 | The slippage protection in V3FactoryOwner.sol#claimFees() function isn’t efficient when multiple fee tiers are present | Low/NC |
The setting of admin in UniStaker.sol#setAdmin()
is implemented as follow:
```solidity function setAdmin(address _newAdmin) external { _revertIfNotAdmin(); _setAdmin(_newAdmin); } ```
The setting of admin in V3FactoryOwner.sol#setAdmin()
is implemented as follow:
```solidity function setAdmin(address _newAdmin) external { _revertIfNotAdmin(); if (_newAdmin == address(0)) revert V3FactoryOwner__InvalidAddress(); emit AdminSet(admin, _newAdmin); admin = _newAdmin; } ```
As we can see the logic for setting the admin in both V3FactoryOwner.sol
and UniStaker.sol
contracts differs. This inconsistency can create confusion and potential security risks if the contracts are expected to operate under a unified governance model (very very low likelihood). Additionally all of us know that inconsistency isn't good thing.
Manual code review
Ensure that the admin setting logic is consistent across both contracts.
My personal suggestion is to follow the admin setting logic of UniStaker.sol
contract. That is, create internal _setAdmin()
function in V3FactoryOwner.sol
contract that do the zero address check. emit event and set new admin.
The documentation states that Uniswap Governance should be the admin
of the V3FactoryOwner
contract, but there's no enforcement of this role in the code or deployment scripts. This lack of enforcement could lead to governance issues or misalignment with the intended administrative control.
In the documentation writes the following: “Uniswap Governance is the
admin
of theV3FactoryOwner
contract.” (here)
Manual code review and analysis of deployment scripts
Ensure that the deployment scripts or initial setup functions explicitly set Uniswap Governance as the admin
of the V3FactoryOwner
contract to align with the documented governance model.
UniStaker.sol#_withdraw()
functionThe comment incorrectly states "overflow prevents withdrawing more than balance" when it should accurately describe the situation as preventing underflow. This miscommenting could lead to confusion about the contract's safety mechanisms.
deposit.balance -= _amount; *// overflow prevents withdrawing more than balance` →* `deposit.balance -= \_amount; *// underflow prevents withdrawing more than balance\*
Manual code review
Correct the comment to accurately reflect that it is an underflow check: // underflow prevents withdrawing more than balance
.
The recommendation pertains to the UniStaker.sol
contract within the UniStaker Infrastructure Protocol, specifically regarding the enhancement of the rewards distribution mechanism. The current implementation does not include a function to adjust the rewards duration dynamically. For reference, see the Synthetix StakingRewards.sol
implementation, which includes a setRewardDuration()
function: Synthetix StakingRewards.sol#L141-L148.
The UniStaker.sol
contract manages the distribution of rewards to stakers, with a fixed reward duration defined at deployment. This fixed duration lacks flexibility in adjusting to changing conditions or strategies that may benefit the protocol and its participants. In contrast, the Synthetix StakingRewards.sol
contract includes a mechanism for dynamically adjusting the reward duration, offering greater adaptability.
The ability to adjust the rewards duration can have several impacts:
Note: This is common practice staking contracts that follow the synthetix logic
Manual Code Review
Step 1. Change the REWARD_DURATION
constant in UniStaker.sol
contract to public state variable with initial value of 30 days
.
Step 2. Implement a setRewardDuration()
function in the UniStaker.sol
contract, similar to the Synthetix implementation. This function should allow the protocol's governance or designated admin to adjust the reward duration.
Example Implementation:
contract UniStaker { // Existing contract variables and functions - uint256 public constant REWARD_DURATION = 30 days; + uint256 public rewardDuration = 30 days; // Assuming this exists + event RewardDurationUpdated(uint256 newDuration); // Constructor and other functions /** * @notice Updates the reward duration for the staking contract. * @param _newDuration The new reward duration in seconds. */ function setRewardDuration(uint256 _newDuration) external { require(msg.sender == admin, "UniStaker: Unauthorized"); require(_newDuration > 0, "UniStaker: Invalid duration"); require( block.timestamp > rewardEndTime, "Previous rewards period must be complete before changing the duration for the new period" ); rewardDuration = _newDuration; emit RewardDurationUpdated(_newDuration); } }
The mappings beneficiaryRewardPerTokenCheckpoint
and isRewardNotifier
in UniStaker.sol
do not follow the naming conventions as other mappings in the contract, leading to code inconsistency and readability issues.
Manual code review
Do the beneficiaryRewardPerTokenCheckpoint
and isRewardNotifier
mappings in UniStaker.sol
contract named mappings as all other mappings are named mappings.
else
BlockThe use of an unnecessary else
block for a return statement can slightly increase complexity and reduce code readability. Simplifying control structures where possible can enhance code clarity.
Manual code review
Refactor the conditional to remove the else
block, simplifying the control flow for improved readability.
📁 File: src/UniStaker.sol 7: import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; 8: import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
📁 File: src/V3FactoryOwner.sol 7: import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; 8: import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
The redundant import of IERC20
alongside SafeERC20
increases code verbosity and can lead to confusion about the usage of ERC20 interfaces within the contract.
Manual code review
Consolidate the imports by removing the direct import of IERC20
where SafeERC20
is also imported, as SafeERC20
already includes IERC20
.
📁 File: src/UniStaker.sol /// @audit move 0 to the left 230: if (totalStaked == 0) return rewardPerTokenAccumulatedCheckpoint; /// @audit move 0 to the left 587: if ((scaledRewardRate / SCALE_FACTOR) == 0) revert UniStaker__InvalidRewardRate(); /// @audit move 0 to the left 745: if (_reward == 0) return;
📁 File: src/V3FactoryOwner.sol /// @audit move 0 to the left 96: if (_payoutAmount == 0) revert V3FactoryOwner__InvalidPayoutAmount(); /// @audit move 0 to the left 121: if (_newPayoutAmount == 0) revert V3FactoryOwner__InvalidPayoutAmount();
Constants appearing on the right side of comparisons diverge from the best practice of Yoda conditions. While not critical in Solidity, adhering to consistent practices can improve code readability.
Putting constants on the left side of comparison statements is a best practice known as Yoda conditions. Although solidity's static typing system prevents accidental assignments within conditionals, adopting this practice can improve code readability and consistency, especially when working across multiple languages.
Manual code review
Adopt Yoda conditions by placing constants on the left side of comparisons for consistency and readability.
V3FactoryOwner.sol#claimFees()
function isn’t efficient when multiple fee tiers are presentThe V3FactoryOwner.sol#claimFees()
function is designed to allow the claiming of protocol fees from Uniswap V3 pools. This function plays a crucial role in the management of liquidity and fee collection within the ecosystem. However, the slippage protection in V3FactoryOwner.sol#claimFees()
function isn’t efficient when multiple fee tiers are present
This is that, because the slippage protection mechanism does not account for the variances in liquidity and price impact across different fee tiers. In a multi-tier fee environment, pools can be imbalanced in various directions, and even if funds are supplied in the same proportion, they might provide liquidity at a less favorable price due to these imbalances.
function claimFees( IUniswapV3PoolOwnerActions _pool, address _recipient, uint128 _amount0Requested, uint128 _amount1Requested ) external returns (uint128, uint128) { PAYOUT_TOKEN.safeTransferFrom(msg.sender, address(REWARD_RECEIVER), payoutAmount); REWARD_RECEIVER.notifyRewardAmount(payoutAmount); (uint128 _amount0, uint128 _amount1) = _pool.collectProtocol(_recipient, _amount0Requested, _amount1Requested); // Protect the caller from receiving less than requested. See `collectProtocol` for context. if (_amount0 < _amount0Requested || _amount1 < _amount1Requested) { revert V3FactoryOwner__InsufficientFeesCollected(); } + if (_amount0 * _amount1 > _amount1Requested * _amount2Requested) { + revert Errors.Enigma_RebalanceBelowMinAmounts(mint0Amount, mint0Amount, minDeposit0, minDeposit1); + } emit FeesClaimed(address(_pool), msg.sender, _recipient, _amount0, _amount1); return (_amount0, _amount1); } /// @notice Ensures the msg.sender is the contract admin and reverts otherwise. /// @dev Place inside external methods to make them admin-only. function _revertIfNotAdmin() internal view { if (msg.sender != admin) revert V3FactoryOwner__Unauthorized(); }
#0 - MarioPoneder
2024-03-14T13:49:44Z
#216
#1 - c4-judge
2024-03-14T13:49:49Z
MarioPoneder marked the issue as grade-b
#2 - radeveth
2024-03-16T15:25:06Z
Hey, @MarioPoneder!
Can you just double-check this QA report. Because it has 4/5 Lows and 4/5 NCs, and based on that QA Scoring Guideline, my QA report has a score of 52-65 points, so IMO it definitely should be grade-a
.
Thank you for your understanding.
#3 - MarioPoneder
2024-03-17T19:13:33Z
Hi!
L-1 is NC L-2 is OOS L-3 is NC L-4 is OK
Due to #216 I can still justify grade-b.
#4 - radeveth
2024-03-26T23:53:33Z
Hey, @MarioPoneder! Thanks for the judging! It was difficult one.
After all, and since we see that there are wardens with only one medium issue submitted that is downgraded to low and marked as grade-a, imo this QA report should also be marked as grade-a based on the issues submitted.
Please take a look on this! Have a good one!