Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $300,500 USDC
Total HM: 79
Participants: 101
Period: about 1 month
Judge: Trust
Total Solo HM: 36
Id: 242
League: ETH
Rank: 40/101
Findings: 3
Award: $626.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
592.6743 USDC - $592.67
Detailed description of the impact of this finding. BaseV2Minter.getRewards() will always fail if it is skipped by one week by flywheelGaugeRewards. The main problem is that while getRewards() always assume there is a sufficient balance in BaseV2Minter but such assumption is not true when BaseV2Minter.getRewards() is skipped for one week, thus not all the rewards are claimed. As a result, BaseV2Minter.getRewards() will mint less HERMES for the following week. Then from now one, there is always a deficit, and BaseV2Minter.getRewards() will always fail. Many people will lose such rewards as a result.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
BaseV2Minter.getRewards() allows flywheelGaugeRewards to claim rewards weekly.
However, it the function call is skipped for one week, then some HERMES will be left in the contract of BaseV2Minter. As a result, when BaseV2Minter.updatePeriod() gets called, it will mint less HERMES:
This will create a deficit that can never be filled. The amount of HERMES in the BaseV2Minter contract will always be smaller than the amount in weekly
. As a result, BaseV2Minter.getRewards() will always fail from now one.
Our POC code confirms this finding:
weekly
since it will consider the balance is already sufficient for the new week. However, overall, the amount of HERMES minted is only sufficient for one week, not for two weeks. Therefore, when we call BaseV2Minter.getRewards(), it will always revert. As a matter of fact, it will always fail in the future regardless how many times we call BaseV2Minter.updatePeriod().function testMissedOneWeek() public { vm.warp(1 weeks); _baseV2Minter.updatePeriod(); vm.prank(address(_flywheelGaugeRewards)); uint256 rewards = _baseV2Minter.getRewards(); console2.log("rewards: %d", rewards); vm.warp(2 weeks); _baseV2Minter.updatePeriod(); vm.warp(3 weeks); _baseV2Minter.updatePeriod(); // since one week is skipped without claim rewards, now each claim will always faile due to deficit vm.prank(address(_flywheelGaugeRewards)); vm.expectRevert(); rewards = _baseV2Minter.getRewards(); console2.log("rewards: %d", rewards); }
The complete code is here: https://www.dropbox.com/scl/fi/fh9051pjnxp3xgrrmztn3/maia.t.sol?dl=0&rlkey=k3jlrryeyrbrcnfnn9j4ntvl2
VSCode
Always mint the required HERMES BaseV2Minter.updatePeriod():
function updatePeriod() public returns (uint256) { uint256 _period = activePeriod; // only trigger if new week if (block.timestamp >= _period + week && initializer == address(0)) { _period = (block.timestamp / week) * week; activePeriod = _period; uint256 newWeeklyEmission = weeklyEmission(); weekly += newWeeklyEmission; uint256 _circulatingSupply = circulatingSupply(); uint256 _growth = calculateGrowth(newWeeklyEmission); uint256 _required = _growth + newWeeklyEmission; /// @dev share of newWeeklyEmission emissions sent to DAO. uint256 share = (_required * daoShare) / base; _required += share; - uint256 _balanceOf = underlying.balanceOf(address(this)); - if (_balanceOf < _required) { - HERMES(underlying).mint(address(this), _required - _balanceOf); - } + HERMES(underlying).mint(address(this), _required); underlying.safeTransfer(address(vault), _growth); if (dao != address(0)) underlying.safeTransfer(dao, share); emit Mint(msg.sender, newWeeklyEmission, _circulatingSupply, _growth, share); /// @dev queue rewards for the cycle, anyone can call if fails /// queueRewardsForCycle will call this function but won't enter /// here because activePeriod was updated try flywheelGaugeRewards.queueRewardsForCycle() {} catch {} } return _period; }
Math
#0 - c4-judge
2023-07-09T08:28:03Z
trust1995 marked the issue as duplicate of #737
#1 - c4-judge
2023-07-09T08:28:09Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:22:21Z
trust1995 changed the severity to 2 (Med Risk)
🌟 Selected for report: Madalad
Also found by: 0xCiphky, 0xSmartContract, 8olidity, BPZ, Breeje, BugBusters, Kaiziron, MohammedRizwan, Oxsadeeq, Qeew, RED-LOTUS-REACH, T1MOH, brgltd, chaduke, giovannidisiena, lsaudit, peanuts, tsvetanovv
10.4044 USDC - $10.40
Detailed description of the impact of this finding.
TalosBaseStrategy.deposit()
lacks slippage control for the users, as a result, a user might loss funds due to price change at Uniswap V3 pools.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
TalosBaseStrategy.deposit()
allows a user to submit liquidity to a Uniswap V3 pool and in return the user will receive shares for the TalosBaseStrategy
contract:
The amount of return shares is determined in the amount of liquidity, which in turn, is determined by function nonfungiblePositionManager.increaseLiquidity()
. Look at the function of increaseLiquidity()
, we can see that the amount of liquidity is not only affected by amount0Desired
, and amount1Desired
, but also the price ration of token0 and token1:
increaseLiquidity()
calls addLiquidity()
:
At L74, the amount of liquidity
depends on sqrtPriceX96
, the current price of token0 vs token1.
Therefore, the price of Uniswap V3 pool might be manipulated and the deposit()
transaction might front run, as a result, the user might get less liquidity, and thus less shares than expected, a loss of funds scenario.
VScode
There should be a paramter minShares
for the deposit()
function so that the user can have the slippage control.
Timing
#0 - c4-judge
2023-07-09T10:54:11Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-09T10:54:16Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-13T11:33:08Z
0xLightt marked the issue as sponsor confirmed
#3 - 0xLightt
2023-07-13T11:37:45Z
To address this issue we believe letting the user pass through amount0Min
and amount1Min
does same thing and requires less logic from external actors
#4 - c4-judge
2023-07-25T13:49:19Z
trust1995 marked the issue as selected for report
#5 - c4-judge
2023-07-26T13:15:16Z
trust1995 marked the issue as not selected for report
#6 - c4-judge
2023-07-26T13:16:45Z
trust1995 marked the issue as duplicate of #577
🌟 Selected for report: Kamil-Chmielewski
Also found by: ByteBandits, Co0nan, Madalad, T1MOH, Udsen, Voyvoda, bin2chen, chaduke, jasonxiale, kutugu, said, xuwinnie, zzebra83
23.9127 USDC - $23.91
Detailed description of the impact of this finding.
UniswapV3Staker.restakeToken()
calls _unstakeToke()
with isNotRestake
flag as true instead of false, as a result, non-owner users CANNNOT call restakeToken even if the block time is after the end time of the incentive.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
UniswapV3Staker.restakeToken() allows a user to restake a token in a new incentive. In particular, a non-owner user can call restakeToken if the block time is after the end time of the incentive.
However, at L342, the flag isNotRestake is set to true instead of false. We need set it to false since we are in the process of Restake.
As a result, non-owner users CANNNOT call restakeToken even if the block time is after the end time of the incentive. To see this, we look at L374 of the _unstakeToken()
function.
L374 will always revert for non-owner users since isNotRestake == true even if the block time is after the end time of the incentive. Therefore, the impact is that, non-owner users CANNNOT call restakeToken even if the block time is after the end time of the incentive.
VSCode
Set isNotRestake to false when calling _unstakeToke()
in UniswapV3Staker.restakeToken()
.
function restakeToken(uint256 tokenId) external { IncentiveKey storage incentiveId = stakedIncentiveKey[tokenId]; - if (incentiveId.startTime != 0) _unstakeToken(incentiveId, tokenId, true); + if (incentiveId.startTime != 0) _unstakeToken(incentiveId, tokenId, false); (IUniswapV3Pool pool, int24 tickLower, int24 tickUpper, uint128 liquidity) = NFTPositionInfo.getPositionInfo(factory, nonfungiblePositionManager, tokenId); _stakeToken(tokenId, pool, tickLower, tickUpper, liquidity); }
Error
#0 - c4-judge
2023-07-09T11:43:55Z
trust1995 marked the issue as duplicate of #745
#1 - c4-judge
2023-07-09T12:34:26Z
trust1995 marked the issue as satisfactory