Day 37:
I'm pretty time poor today and the Ethernaut challenges are becoming frustrating so I've decided to read an audit report by OpenZeppelin published this month and crack on with multiple challenges tomorrow.
Background
Taking from the audit report: UniswapV3 is an Automated Market Maker (AMM) that allows users to buy tokens from token-specific pools at automatic prices.
As an aside, an AMM helps to create a system of asset liquidity (typically a token) where anyone can contribute to the pool. Benefits often cited are peer-to-peer exchanges greatly reducing cost as a result of no intermediary and lightening quick transfers. As foreshadowed, the drawback of a liquidity pool created by the AMM is that anyone can contribute to it, meaning if a malicious actor decided to manipulate price (a function of supply and demand within the asset pool) then it would be readily available for the actor to do so.
Uniswap decided to implement a feature that assures counterparties of the liquidity in the market being permanent / remain in the market for a long time to counteract this type of behaviour.
System Overview
- The market liquidity position is converted into an NFT using the market's
NonfungiblePositionManager
; and - this NFT is transferred to the (
UNCX_ProofOfReservesV2_UniV3
) contract.
The above allows owners to lock their tokens and collect transaction fees from their underlying market position but they are unable to take back ownership of its liquidity position. The practical effect of this is that the owner wouldn't be able to suddenly remove the liquidity from the market until a pre-specified time in the future.
I recommend reading the audit report for a further breakdown of the roles creates to manage the above system.
Key Issues and Takeaways
Missing slippage protection for locking liquidity
This was flagged as a critical issue by OZ. At its core, an exploit was possible because the liquidity between the tickUpper
(the upper tick of the position for which to burn liquidity) and tickLower
(similar to tickUpper but lower band) decreases while the liquidity in any tick outside that pre-set range increases. What does this mean? Basically, if the attack manipulates the pool such that it locks its liquidity outside the Upper/Lower tick, the liquidity position could be comprised solely of one token (defeating the whole purpose of a swap) and the attacker can make the victim deposiut the full amount of liquidity by transferring a small amount.
The fix to this was basic - put slippage parameters to the relevant lock
function.
Anybody Can Remove Tokens Accidentally Sent to the Contract
The adminRefundEth
and adminRefundERC20
functions were included to allow owners to withdraw tokens which are accidentally sent to the contract. However, the call to _convertPositionToFullRange
(originally included to ensure positions converted to an NFT were a full range position) will send the entire balanceOf
both pool tokens to either the new locked liquidity position or refund it to the dustRecipient
. Thus, a user who calls lock
gets all the tokens that are stuck in the contract credited to themselves by setting the dustRecipient
to an address they control. What this practically means is that anybody could withdraw tokens in the contract by calling lock
on a non-full-range NFT position where one pool token corresponds to the token which is stuck in the contract.
transfer
and send
Calls Are No Longer Considered Best Practice
This was an issue raised in previous blog posts and it was cool to see this in an audit report. When transfer or send calls are used to transfer ETH to an address, they forward a limited amount of gas. As explained in a previous post (Days 19 and 20) given the everchanging cost associated with gas prices for EVM operations, code execution on the receiving end of these calls cannot be guaranteed in perpetuity.
Taken directly from OZ:
Instead of using transfer or send, consider using address.call{value: amount}("") or the sendValue function of the OpenZeppelin Address library to transfer ETH.
A series of other issues were mitigated by simply considering the scope of roles, set to govern the pool itself (e.g. Arbitrary Position Managers, Collector Role Is Not Relinquished During Lock Transferral).
Edited: I felt that a few issues I didn't raise were worthwhile including after a second read of the report.