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: 35/47
Findings: 1
Award: $266.89
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
266.8911 USDC - $266.89
When undertaking the review of the UniStaker Infrastructure protocol the following steps were taken:
A total of approximately 24 hours was spent conducting the security review of this codebase over a 6 day period.
The UniStaker Infrastructure codebase lays out an approach to allow $UNI holders to stake their tokens to earn fees from Uniswap V3 pools in the event that the Uniswap fee switch is turned on.
This is separated across 3 contracts:
The UniStaker contract contains the logic to allow users to stake/withdraw their $UNI tokens and claim rewards earned. Additionally the user is able to alter the addresses of their deposits delegatee and beneficiary. As well as these user facing functions the contract is also responsible for calculating rewards, the method of which borrows heavily from the Synthetix Staking formula.
The V3FactoryOwner contract is responsible for setting the fees for V3 pools. It is also responsible for converting the fees (denominated in amounts of the two pairs in a given pool) into one PAYOUT_TOKEN
. It does this by allowing anyone (likely an MEV seacher) to purchase the earned fees for a given pool in return for PAYOUT_TOKEN
providing that the REWARD_RECEIVER
(the deployed UniStaker contract instance) is sent payoutAmount
of tokens.
The DelegationSurrogate contract is a simple contract that allows UniStaker::STAKE_TOKEN
s staked in the UniStaker contract to be held whilst their voting power is delegated to a specific address for the purpose of Uniswap governance voting. The UniStaker contract has the power to transfer tokens in and out of the DelegationSurrogate contract. Each address that is set as delegatee for a deposit will have it's own instance of the DelegationSurrogate contract deployed.
There are a total of 3 different roles that interact with the system:
UniStaker::admin
- Has the power to add/remove addresses that are valid rewardNotifiers
V3FactoryOwner::admin
- Has the power to set the required payoutAmount
, enable the factory's fee amount and set the protocol fees for a given V3 pool.
The key user actions for stakers involve staking/withdrawing STAKE_TOKEN
andclaiming rewards (in REWARD_TOKEN
). They are also able to set/alter the delegatee of their token's voting power for the purposes of uniswap governance and a beneficiary address, which will be the address eligible to claim rewards earned from their deposit.
Fee purchasers will be able to redeem the fees generated by a Uniswap V3 Pool by calling V3FactoryOwner::claimFees
(after approving the V3FactoryOwner address payoutAmount
of tokens). This function requires the fee purchaser send payoutAmount
of PAYOUT_TOKEN
to the UniStaker contract, in return they will receive the fees generated by the specified _pool
. This creates an MEV opportunity whenever the total feels generated by a pool are greater than payoutAmount
plus the gas cost of calling claimFees
it will be profitable for an MEV searcher to claim the fees, making a small profit.
The following diagrams map out the control flow of the key user facing functionality and a brief overview of their affects on the contracts state.
Key user actions:
In the current implementation of the UniStaker contract there is no way for a user to retreive the depositId
s associated with their address. This leads to the user either having to keep track of it themselves when first staking or rely on a UniStaker frontend to have stored this data for them. Adding a mapping that allows users to find their related deposit ids on chain would make it easier for users to interact with the protocol in the event that the frontend staking site were to go down.
V3FactoryOwner::payoutAmount
being constant across pools could lead to issuesIn V3FactoryOwner::claimFees
it is always required that payoutAmount
is sent to the REWARD_RECEIVER
(the UniStaker implementation) however this could be problematic given the different amounts of volumes across different Uniswap V3 Pools, meaning pools will take different amounts of time before calling claimFees
becomes profitable.
An example to highlight this, consider the differences in volume of these two pools at the time of writing this report (29/02/24):
In the current implementation both these pools would have to generate the same amount of fees for a user to be incentivised to call V3FactoryOwner::claimFees
. If the payoutAmount
is set too low then the arbitrage available to the fee purchaser will increase (decreasing the the value passed onto UNI stakers), whilst if payoutAmount
is set too high there's a chance the fees earned take a very long time to be passed onto UNI stakers, or worse still, never end up being passed on at all (for example if a different pool for that pair takes all the trading volume away from it).
To avoid the risks of typos or other mistakes leading to the incorrect admin
being set for the UniStaker
and V3FactoryOwner
contracts, its recommended that the protocol makes use of a two step ownership method for transferring the admin role.
As shown in the above control flow diagrams, the function lastTimeRewardDistributed
is called 3 times each time the user stakes/withdraws/claims rewards and 4 times when calling alterBeneficiary
. While it is not a particularly gas intensive function it does include one SLOAD per call, so if the protocol wanted to improve the gas efficiency of the contracts this could be an area to look into by caching the result of lastTimeRewardDistributed
once and then passing it around as necessary. However, doing so might impact the readability of the codebase so it's a tradeoff best decided on by the protocol team.
While the the UniStaker
and V3FactoryOwner
contracts do have admin
roles, the admin is quite limited in their powers. They don't have any ability to transfer funds out of their related contracts. As these powers will also be in the hands of the Uniswap DAO Governance the contracts should be considered sufficiently decentralised.
The protocol is well designed resulting in a concise implementation which acheives in executing the key features of the protocol. The brevity of the code results in a codebase with a very high readability. The benefits of this are two fold, firstly it means the codebase has very good auditability as someone approaching the code can get up to speed with its implementation quickly. Secondly, and more importantly, the small surface area of the codebase decreases the chance of vulnerabilities being caused by developer errors.
The codebase has complete NATSPEC comments as well as useful inline comments explaining any logic that might not be clear on first glance. On top of this the explainer docs given in the contest overview were clear and gave researchers a good idea of what to expect from the codebase from the get go.
The codebase has a comprehensive test suite which includes fuzzing, invariant testing and integration tests. As well as this the test suite's layout and collection of useful helper functions makes it easy for auditors to plug in their own edge case tests where necessary.
24 hours
#0 - c4-sponsor
2024-03-11T17:49:10Z
apbendi (sponsor) acknowledged
#1 - c4-judge
2024-03-14T18:29:33Z
MarioPoneder marked the issue as grade-a