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: 11/47
Findings: 2
Award: $716.32
π 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
Judge has assessed an item in Issue #83 as 2 risk. The relevant finding follows:
[L-04] Note that collectProtocol collects everything but 1 wei. Fee collectors who arenβt aware may get their transactions reverted
#0 - c4-judge
2024-03-14T14:14:25Z
MarioPoneder marked the issue as duplicate of #34
#1 - c4-judge
2024-03-14T14:14:29Z
MarioPoneder marked the issue as satisfactory
#2 - c4-judge
2024-03-26T22:51:31Z
MarioPoneder marked the issue as grade-b
π Selected for report: roguereggiant
Also found by: 0xepley, Aamir, Al-Qa-qa, LinKenji, MSK, McToady, Myd, SAQ, Sathish9098, ZanyBonzy, aariiif, cudo, emerald7017, fouzantanveer, hassanshakeel13, hunter_w3b, ihtishamsudo, kaveyjoe, peanuts
22.023 USDC - $22.02
UNI token holders can now stake their tokens and earn rewards.
This rewards is actually from fees accrued in the Uniswap pool.
Usually, uniswap pool has 2 tokens, so it is very hard for the fees to be distributed properly to the UNI holders
To combat this issue, another party, the claimer, collects these two tokens and give an equivalent amount (or slightly less, to incentivize this action) of a single token to the Unistaker pool as the rewards to be given to the UNI stakers.
This amount to be given is set by the admin. (eg 10000 USDC) Only the admin can change this amount.
The claimer will wait until 10000+ USDC worth of fees is accrued and exchange their one token with the two fee tokens
Their one token will become rewards for the UNI token holders
For example, the WBTC-UNI pool has accrued some fees, eg 0.01 BTC and 100 UNI. It's very hard to give these two tokens to the users.
Assume 0.01 BTC and 100 UNI is worth 5000 USDC each. Assume WETH is worth 1000 each.
A claimer comes in, sees that the pool has 0.01 BTC and 100 UNI worth 10000 USDC. He collects these two tokens in exchange for 10 WETH, worth 10000 USDC.
This 10 WETH will be the reward tokens, and UNI token holders who stake their tokens will earn this 10 weth proportionately
Note that there will always be a claimer because if someone doesn't want to swap their WETH for the fees, then the fees will continue to accrue, and once it reaches a point where it is profitable, the claimer will want to exchange the fees for the reward amount
The reward distribution typically lasts for 30 days. Rewards are counted every second. A user who deposits late (eg 15 days) will not get rewards from day 1 to 15.
UNI token holders can exit their position anytime.
UNI token holders also have the capability to increase their stake, or change their reward (beneficiary) address.
UNI tokens can also delegate their votes to a delegatee of their choice when staking.
This whole protocol is an ongoing process. Claimers will wait to claim their fees in exchange for the payout token, and the UNI stakers waits until the reward tokens is transferred into the Unistaker contract to earn rewards.
The protocol will only not work if there are no more fees to claim from the pools that enable fees (which is highly unlikely)
Contract | Function | Access | Comments |
---|---|---|---|
UniStaker | constructor | - | Delegates all token in the contract to the delegatee, and approves Unistaker to use all the Uni tokens. |
Checks
Potential Issues
_token.delegate(_delegatee);
call, so the delegation can only be called once. Users can stake more into the surrogate contract, but delegate will not be called. (Medium)Contract | Function | Access | Comments |
---|---|---|---|
UniStaker | setAdmin | onlyAdmin | No two step transfer of ownership |
UniStaker | setRewardNotifier | onlyAdmin | Checks whether contract is ready to receive reward via notifyRewardAmount |
UniStaker | lastTimeRewardDistributed | view | Called in global checkpoint reward. Sets lastCheckpointTime |
UniStaker | rewardPerTokenAccumulated | view | Called in global checkpoint reward. Sets rewardPerTokenAccumulatedCheckpoint |
UniStaker | unclaimedReward | view | Calculates the unclaimed reward of the beneficiary |
UniStaker | stake | public | Entry point of users. Calls _stake |
UniStaker | stake | public | Entry point of users. Calls _stake. Sets the beneficiary |
UniStaker | permitAndStake | public | Entry point of users. Calls _stake. Sets the beneficiary. Note permit frontrun issue |
UniStaker | stakeOnBehalf | public | Helps another person call stake. Must be a trusted signature |
UniStaker | stakeMore | public | Must already have a stake. Only can stake more for oneself |
UniStaker | permitAndStakeMore | public | Must already have a stake. Only can stake more for oneself |
UniStaker | stakeMoreOnBehalf | public | Must already have a stake. Must be a trusted user |
UniStaker | alterDelegatee | public | Must already have a stake. Surrogate contract transfers UNI tokens to other Surrogate |
UniStaker | alterDelegateeOnBehalf | public | Same as above. Caller must be trusted |
UniStaker | alterBeneficiary | public | Changes the reward recipient. Both previous and current recipient will update their rewards |
UniStaker | alterBeneficiaryOnBehalf | public | Same as above. Caller must be trusted |
UniStaker | withdraw | public | Withdraws UNI tokens from surrogate contract to the deposit owner |
UniStaker | withdrawOnBehalf | public | Same as above. Caller must be trusted |
UniStaker | claimReward | public | Only beneficiary can call |
UniStaker | claimRewardOnBehalf | public | Claims reward for the beneficiary. |
UniStaker | notifyRewardAmount | Claimer | Called by Claimer in V3FactoryOwner. Reward tokens is deposited into Unistaker |
UniStaker | _fetchOrDeploySurrogate | internal | Deploys a new surrogate contract if delegatee doesn't exist yet. |
UniStaker | _stakeTokenSafeTransferFrom | internal | Safe transfers token |
UniStaker | _fetchOrDeploySurrogate | internal | Deploys a new surrogate contract if delegatee doesn't exist yet. |
UniStaker | _useDepositId | internal | Called in _stake. Increments depositId value by 1 |
UniStaker | _stake | internal | Beneficiary gets earningPower, creates a Deposit struct, call checkpoint global and reward |
UniStaker | _stakeMore | internal | Calls checkpoint global and reward |
UniStaker | _alterDelegatee | internal | Deploys new surrogate if doesn't exist. Updates deposit struct with new delegatee |
UniStaker | _alterBeneficiary | internal | Updates previous and current beneficiary, update deposit struct with new beneficiary |
UniStaker | _withdraw | internal | Opposite of _stake, transfers withdraw amount to the deposit.owner |
UniStaker | _claimReward | internal | Beneficiary will claim the reward. Reward will be set to zero |
UniStaker | _checkpointGlobalReward | internal | Updates rewardPerTokenAccumulatedCheckpoint, updates lastCheckpointTime |
UniStaker | _checkpointReward | internal | Updates unclaimedRewardCheckpoint, updates beneficiaryRewardPerTokenCheckpoint |
Scenarios Checked
_checkpointReward()
is called and beneficiaryRewardPerTokenCheckpoint[_beneficiary] = rewardPerTokenAccumulatedCheckpoint
. That means that from now on, although the number is extremely large, it doesn't matter because the calculation starts from that second.unclaimedReward()
calculationYes, rewards are checked and distributed proportionately. Assume 300 Reward tokens for 30 days. User A is the only user with 100 UNI staked. 10 days later, user B stakes 100 UNI. 10 days after this, User C stakes 800 UNI.
First 10 days, user A gets 100 Reward tokens
Second 10 days, user A gets 50 reward tokens, user B gets 50 reward tokens
Last 10 days, user A gets 10, user B gets 10, user C gets 80.
This is because everytime someone stakes, they will start with rewardPerTokenAccumulatedCheckpoint as their beneficiaryRewardPerTokenCheckpoint[_beneficiary]
. If they decide to claim the second they stake, rewardPerTokenAccumulated()
- beneficiaryRewardPerTokenCheckpoint[_beneficiary]
= 0. They will start earning their share the second they start staking
unclaimedReward()
simplay adds the previous unclaimed rewards if (_amount0 < _amount0Requested || _amount1 < _amount1Requested) {
check. If the claimer do not get the fees they asked for, they will not transfer their reward tokens to the UniStaker contract.earningPower[_beneficiary] * (rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary])
will return zero as rewardPerTokenAccumulated() - beneficiaryRewardPerTokenCheckpoint[_beneficiary]
is zero.Potential Issues
Contract | Function | Access | Comments |
---|---|---|---|
V3FactoryOwner | setAdmin | onlyAdmin | No two step transfer of ownership |
V3FactoryOwner | setPayoutAmount | onlyAdmin | Sensitive function, claimers will exchange payout amount for fees |
V3FactoryOwner | enableFeeAmount | onlyAdmin | Enables collection of fees, called to UniswapV3Factory.sol |
V3FactoryOwner | setFeeProtocol | onlyAdmin | Sets fee amount, called to UniswapV3Pool.sol |
V3FactoryOwner | claimFees | Public | Called by anyone who wants to collect fees in exchange for payout amount |
Notes
Checks
Potential Issues
createPool()
in UniswapV3Factory (Medium)Staking and withdrawing
Altering Delegatees and Beneficiaries
Collecting Rewards
setRewardNotifier()
. If it is not set, the whole protocol will not worksetPayoutAmount()
. Able to grief the claimers and the protocol itself.025 hours
#0 - c4-sponsor
2024-03-08T16:47:05Z
wildmolasses (sponsor) acknowledged
#1 - thebrittfactor
2024-03-08T16:50:33Z
For transparency, the sponsor inadvertently marked as acknowledged
. Removing label until the team has a chance to review further.
#2 - c4-judge
2024-03-14T18:17:22Z
MarioPoneder marked the issue as grade-c
#3 - c4-judge
2024-03-14T18:17:35Z
MarioPoneder marked the issue as grade-b