Open c4-bot-4 opened 9 months ago
MarioPoneder marked the issue as primary issue
apbendi (sponsor) confirmed
apbendi marked the issue as disagree with severity
This is an issue that, at a minimum, warrants an update to the comments for clear documentation. However, even if deployed exactly as-is, the protocol would function, and an MEV searcher developing a bot would have likely seen the issue an accounted for it during testing. No funds were at risk, nor was the protocol's availability. We consider this a Low Severity issue.
The likelihood of the present issue is High as nearly every MEV searcher will eventually run into this problem when claming fees and has to figure out the cause, which was neither documented not excepted at the time of this audit contest (source of truth).
The impact is Low since it's just a temporary DoS scenario until the MEV searcher has adapted to that.
In the end, it's more than a simple UX issue and natural for MEV searchers to try claiming the maximum fee amounts.
Furthermore, given the very High likelihood despite Low impact, an overall Medium severity is justified,
MarioPoneder marked the issue as satisfactory
MarioPoneder marked the issue as selected for report
The impact is Low since it's just a temporary DoS scenario until the MEV searcher has adapted to that.
I think there is a small misconception about this issue: it won't cause a DoS. Users are free to request 1 wei less themselves, even in the same block. I do not think that leaving a 1 wei reward on the table can be considered a reasonable loss.
The Sponsor is correct. No funds are at risk (this applies to both the protocol and the users), and the protocol's availability is not affected.
This is a user error, as the user should have tested and simulated their transaction before executing it:
Non privileged users are expected to preview their transactions to protect against input mistakes. Anyone using modern tooling will have previews built into their toolset.
But even if they execute this transaction, it will simply revert. They could retry immediately with a smaller _amount0Requested/_amount1Requested
. There isn't any impact here.
Due to these reasons, I believe this is a low-severity issue, as it simply improves the UX for the users.
The impact is not limited to a DoS. It will waste the MEV gas fees which are expensive in mainnet and the MEV will lose the auction.
It will waste the MEV gas fees which are expensive in mainnet and the MEV will lose the auction.
This impact was never stated in the original submission and moreover the report should be labelled "Gas-saving" and classified as such .
Agree with other wardens, that this issue looks like gas-saving issue or maybe qa. Its exactly not medium.
The text describes only a feature of the protocol, not a vulnerability
The premise of this issue is false or OOS.
This issue is based on the assumption/expectation of how much can be claimed. V3FactoryOwner
is completely agnostic about this amount. The caller would therefore have to refer to the UniswapV3Pool
, and should discover that the maximum claimable amount is protocolFees.tokenX - 1
. If this is unclear enough to be considered an issue, then it pertains to UniswapV3Pool
, which is OOS for this audit.
About half of the duplicates of this issue were reported as QA, and I am certain many more thought of this very issue but chose not to report it (myself included), assessing it as QA/OOS, for the above reasons.
As I reported this issue as QA, this issue should be considered of Low severity.
Before collecting the fee, fee collectors (EOA/MEV) should understand the details of the maximum claimable amount when making fee claims.
Therefore, this issue only presents ambiguity regarding the claimed amounts due to the absence of documentation outlining this specific point.
As many have stated the impact is overinflated for the reasons listed above, I just want to add that in all test cases written by sponsor teams, they subtract 1 from the fee, which leads to the conclusion that the problem is known to the sponsor and accepted before the contests start.
This is not only a DOS problem, as described in #372 , when multiple MEV searchers auction, due to transaction execution timing issues, tx executed with low gas delay may be executed normally, and the auctioneer pays high gas but loses the auction, low gas wins the auction, which affects the auctioneer's income.
I want to answer some points that are mentioned by some wardens, related to the issue, its severity, and its validity.
Dispute
: The value requested is adjusted by the caller, so some wardens said that this issue would only happen if the caller called by providing all fees only.
Answer
: As I mentioned in the report:
But if the caller requests to claim all protocol fees, which is the best case for him to earn the maximum profit
the caller will simply request to take all fees from both tokens. And there are MEVs (Uniswap may have one) that will track the amount of fees collected and compare it to the payoutAmount, and call the function by reading the values from the pool itself (to claim all the fees).
So the value requested will be all fees collected to get the maximum profit, and as we said MEW will track the amount of fees collected, and it will read the value from the pool itself. which makes calling the function with all fees is the command case and most of the callers will call it with the max fees collected.
So as the judger said the likelihood of this issue is HIGH.
Dispute
: You did not stated that the MEV (caller) will lose gas.
Answer
: As we stated in the title *will revert *
, and in the report
the transaction will get reverted
And reverting tx consumes gas (most of us know this point). And since the context is an MEV, the gas provided will be HIGH, and again all of us know that MEV transactions are subjected to race-condition
, which means the provided gas is HIGH. Besides that, the judger and the Sponsor did not dispute this point.
Dispute
: The DoS Will not occur, where users can request less that the amount of the collected fee.
Answer
: As we mentioned in the first point, calling the function with the maximum value is the default behavior, and subtracting 1 wei is not mentioned in the docs as the Judger said. So The DoS will occur, but for how long?
The DoS is temporal for MEVs that call the function by providing the value itself, and this is true. But other MEVs will be in permanent DoS, which are the MEVs that are designed to read the fees value before calling.
call the function by reading the values from the pool itself (to claim all the fees)
So the MEVs that are designed as smart contracts to read the values before calling (to extract the maximum value), will be in permanent DoS not temporal.
These MEVs will be designed to provide a check of the profit, as they will depend on always passing the notifier check.
Likelihood is HIGH, severity bounds between MEDIUM and LOW, MEDIUM severity is fair.
To further add on to Al-Qa-Qa point, a DoS will lead to fees being accrued in the pools being unclaimed and that will lead to a loss of funds for staker.
To further add on to Al-Qa-Qa point, a DoS will lead to fees being accrued in the pools being unclaimed and that will lead to a loss of funds for staker.
This is a blatant attempt to further inflate the issue which was never stated in the first place. I still do not see how this can be of any other severity than "gas" .
Dear @quentin-abei, there is no "attempt" to do anything here. What I am stating is a fact, the Unistaker model relies on MEV bots to claim the fees as fast as possible, if there is a temporal DoS then the fees will accrue without the staker being rewarded. How much fees will accrue without being claimed? I am not sure entirely, but I will leave it to the judge to determine the final ruling.
And the severity you are talking about is "gas optimization" which refers to optimizing smart contracts for gas.
I also would like to add a comment as the author of the #45
The whole reward logic is about creating a race between MEV bots. The result of this bug is not simply losing 1 wei, it is losing the race, which is losing all possible profits in that pool and losing gas during transaction, just because of 1 wei.
I disagree this being user's fault. It is an integration issue between two parts of Uniswap. UniStaker is not integrated with UniCore in a seamless way. Uniswap itself introduces 1 wei decrement to save gas on their pool, and the Uniswap itself should match with this behaviour in their Unistaker part.
Kind regards.
As many have stated the impact is overinflated for the reasons listed above, I just want to add that in all test cases written by sponsor teams, they subtract 1 from the fee, which leads to the conclusion that the problem is known to the sponsor and accepted before the contests start.
This is a good point.
Add to that the fact that the comment over the slippage check in claimFees()
explicitly says "See collectProtocol
for context.".
Add also the fact that the MEV searcher absolutely must weigh the gas costs of his operation. That is, he must look into both claimFees()
and collectProtocol()
, whether by inspection or simulation. I am therefore not convinced he is highly likely to miss the - 1
after all.
In any case it is only a matter of documentation. The code itself is fully valid and functional as it is, and any suggested change is a design proposal rather than a mitigation of a bug.
We strongly disagree with this issue being classified as a Medium Severity. Other wardens have done a good job laying this out, but to summarize briefly:
At most we believe this could be called a low severity issue, and even here we believe that is a generous characterization. Others have made the case it should be called QA or gas savings, and their arguments are reasonable.
Thanks for all the comments with alternatingly opposing opinions, that's what PJQA is about!
Please consider the following aspects:
revert
and cannot be considered a user error.Consequently, Medium severity will be maintained.
MarioPoneder marked issue #45 as primary and marked this issue as a duplicate of 45
- Trying to claim max. possible fees, which is reasonable, will result in a
revert
and cannot be considered a user error.
@MarioPoneder, this is the core of the argument, which is otherwise sound and correct. But what is the definition of "max. possible fees"? Why is it correct to say it is protocolFees.tokenX
rather than the maximum amount claimable according to collectProtocol()
? I find no documentation on this, and therefore no assumptions can be made outside of the code itself.
I agree it reasonably should have been protocolFees.tokenX
, but that is what it should have been in collectProtocol()
, which is OOS.
The only way to "fix" this issue is to allow a 1 wei slippage. But this just propagates the exact same integration issue to the next contract down the line interacting with V3FactoryOwner, expecting no less than _amountXRequested
.
Note also that collectProtocol()
reduces any excessive amountXRequested
to protocolFees.tokenX - 1
. So a user might also expect to be able to claim max fees by using type(uint256).max
. Should claimFees()
therefore do the same and thus allow any slippage?
Why should V3FactoryOwner incorporate a hidden slippage, and thus sacrifice its own integrity, to smooth out a quirk in UniswapV3Pool, only to transfer the same integration issue to the next contract?
MarioPoneder marked the issue as grade-b
MarioPoneder marked the issue as grade-a
Lines of code
https://github.com/code-423n4/2024-02-uniswap-foundation/blob/main/src/V3FactoryOwner.sol#L193
Vulnerability details
Impact
Stakers earn yields when someone claims the Uniswap pool fees and pays the
payoutAmount
WETH, and if the amount of tokens either the first pair or the second is less than the amount requested by the user the transaction reverts.V3FactoryOwner.sol#L181-L198
This check protects the caller of the function from getting less amount than expected, as he pays WETH to take these fees. But if the caller requests to claim all protocol fees, which is the best case for him to earn the max profit, the transaction will get reverted, as the receiving amount will get subtracted by 1 when calling
pool::collectProtocol()
with theamount*Requested == protocolFees.token*
.v3-core/UniswapV3Pool.sol#L848-L868
So the transaction will get reverted and claiming all protocol fees from either the first or the second token will get reverted.
Since the caller pays the
payoutAmount
to claim the fees, the caller will simply request to take all fees from both tokens. And there are MEVs (Uniswap may have one) that will track the amount of fees collected and compare it to thepayoutAmount
, and call the function by reading the values from the pool itself (to claim all the fees). So firing the function with the maximum fees taken by the pool will be the default behavior, and it will get reverted as we illustrated.Proof of Concept
We created a Foundry test that simulates claiming fees from a real uni-V3 pool, here is how to set it up.
virtual
totest/mocks/MockUniswapV3Pool.sol::collectProtocol()
test/V3FactoryOwner.t.sol
Contract
V3FactoryOwner.t.sol
aftertestFuzz_RevertIf_CallerExpectsMoreFeesThanPoolPaysOut
testTesting Script
```solidity contract ClaimFees is V3FactoryOwnerTest { ... Auditor_MockUniswapV3Pool auditor_pool; function test_auditor_claimMaximumFeesReverts() public { auditor_pool = new Auditor_MockUniswapV3Pool(); vm.label(address(pool), "Pool"); uint256 payoutAmount = 1e18; // paying 1e18 to claim reward address caller = makeAddr("caller"); // The caller of the `claimFee` address receipent = makeAddr("receipent"); // The receipent of the pool token pairs fees uint128 amount0 = 0.5e18; // first token pait fees received uint128 amount1 = 0.5e18; // second token pait fees received _deployFactoryOwnerWithPayoutAmount(payoutAmount); payoutToken.mint(caller, payoutAmount); // Updating fees to 0.5e18 for each token pair auditor_pool.setNextReturn__collectProtocol(0.5e18, 0.5e18); vm.startPrank(caller); payoutToken.approve(address(factoryOwner), payoutAmount); console2.log("protocolFees.token0:", auditor_pool.mockFeesAmount0()); console2.log("protocolFees.token1:", auditor_pool.mockFeesAmount1()); console2.log("amount0Requested:", amount0); console2.log("amount1Requested:", amount1); // vm.expectRevert(V3FactoryOwner.V3FactoryOwner__InsufficientFeesCollected.selector); factoryOwner.claimFees(auditor_pool, receipent, amount0, amount1); console2.log(""); console2.log("Call reverted"); vm.stopPrank(); } } ```forge test --mt test_auditor_claimMaximumFeesReverts
Output:
Tools Used
Manual Review + Foundry
Recommended Mitigation Steps
Make 1 wei tolerance when checking for the received value in
V3FactoryOwner::claimFees()
Assessed type
Uniswap