Platform: Code4rena
Start Date: 11/12/2023
Pot Size: $24,150 USDC
Total HM: 19
Participants: 5
Period: 10 days
Judge: 0xLeastwood
Total Solo HM: 6
Id: 312
League: ETH
Rank: 3/5
Findings: 8
Award: $0.00
🌟 Selected for report: 2
🚀 Solo Findings: 0
Data not available
https://github.com/code-423n4/2023-12-particle/blob/main/contracts/protocol/ParticlePositionManager.sol#L399-L406 https://github.com/code-423n4/2023-12-particle/blob/main/contracts/protocol/ParticlePositionManager.sol#L415-L420
A liquidator can manipulate the pool they are swapping in to take any potential left over premium from the borrower.
When liquidating a position the liquidator essentially closes the position on behalf of the borrower for a liquidation reward.
When closing a position the token held by the borrower is potentially traded back to cover the debt to the liquidity provider:
ParticlePositionManager::_closePosition#L399-L406
:
File: contracts/protocol/ParticlePositionManager.sol 399: (cache.amountSpent, cache.amountReceived) = Base.swap( 400: cache.tokenFrom, 401: cache.tokenTo, 402: params.amountSwap, 403: 0, /// @dev we check cache.amountReceived is sufficient to repay LP in below 404: DEX_AGGREGATOR, 405: params.data 406: );
Then these amounts are used to check that the amount swapped will cover repaying the lender:
ParticlePositionManager::_closePosition#L415-L420
:
File: contracts/protocol/ParticlePositionManager.sol 415: if ( 416: cache.amountFromAdd > cache.collateralFrom + cache.tokenFromPremium - cache.amountSpent || 417: cache.amountToAdd > cache.amountReceived + cache.tokenToPremium 418: ) { 419: revert Errors.InsufficientRepay(); 420: }
Where amountTo/FromAdd
is the amounts needed to repay the liquidity borrowed from the liquidity provider.
The issue here is that there can be tokenFrom/ToPremium
left. The liquidation requirement is that only one of the sides needs to be under water. These leftovers will however be taken in the swap. The liquidator can manipulate the pool being swapped in such that amountSpent
and amountReceived
uses all the premium from the borrower.
This would have otherwise been returned to the borrower in the refunds at the end of _closePosition
.
Manual audit
Consider if this is a feature or not.
If it is intended behavior for the liquidator to take what is left of the borrowers premium, consider refunding the liquidator instead of borrower in case of liquidation.
If this is not a feature and the borrower is entitled to whatever premium they have left, consider enforcing better slippage protection on the swap in _closePosition
.
MEV
#0 - c4-judge
2023-12-21T22:01:13Z
0xleastwood marked the issue as duplicate of #56
#1 - c4-judge
2023-12-21T22:16:44Z
0xleastwood marked the issue as duplicate of #26
#2 - c4-judge
2023-12-23T22:27:44Z
0xleastwood changed the severity to 3 (High Risk)
#3 - c4-judge
2023-12-23T22:27:48Z
0xleastwood marked the issue as satisfactory
#4 - 0xleastwood
2023-12-29T13:13:36Z
No clear POC outlined here even if the issue is valid. It's not clear how this would be exploited by an attacker. Giving partial credit.
#5 - c4-judge
2023-12-29T13:13:40Z
0xleastwood marked the issue as partial-50
Data not available
Judge has assessed an item in Issue #49 as 2 risk. The relevant finding follows:
L-05 Some tokens revert on 0 amount transfer ParticlePositionManager::liquidatePosition:
File: protocol/ParticlePositionManager.sol
376: // reward liquidator 377: TransferHelper.safeTransfer(closeCache.tokenFrom, msg.sender, liquidateCache.liquidationRewardFrom); 378: TransferHelper.safeTransfer(closeCache.tokenTo, msg.sender, liquidateCache.liquidationRewardTo); Some tokens, like LEND revert on 0 amount transfer. If the liquidation amount is 0 for this token this would make liquidation impossible for this position.
Recommendation Consider checking if the liquidationAmount > 0 before transferring liquidation reward.
#0 - c4-judge
2023-12-31T13:07:38Z
0xleastwood marked the issue as duplicate of #61
#1 - c4-judge
2023-12-31T13:07:48Z
0xleastwood marked the issue as satisfactory
Data not available
https://github.com/code-423n4/2023-12-particle/blob/main/contracts/libraries/LiquidityPosition.sol#L132-L146 https://github.com/code-423n4/2023-12-particle/blob/main/contracts/libraries/LiquidityPosition.sol#L190-L199 https://github.com/code-423n4/2023-12-particle/blob/main/contracts/libraries/LiquidityPosition.sol#L254-L262
Lack of deadline can cause a transaction to be executed at an unfavorable time. Causing loss for the trader/liquidity provider.
In all three liquidity interactions with uniswap there is a lack of deadline parameter as block.timestamp
is passed:
File: contracts/libraries/LiquidityPosition.sol 132: (tokenId, liquidity, amount0Minted, amount1Minted) = Base.UNI_POSITION_MANAGER.mint( 133: INonfungiblePositionManager.MintParams({ ... // ... other params 144: deadline: block.timestamp 145: }) 146: );
LiquidityPosition::increaseLiquidity
:
File: contracts/libraries/LiquidityPosition.sol 190: (liquidity, amount0Added, amount1Added) = Base.UNI_POSITION_MANAGER.increaseLiquidity( 191: INonfungiblePositionManager.IncreaseLiquidityParams({ ... // ... other params 197: deadline: block.timestamp 198: }) 199: );
LiquidityPosition::decreaseLiquidity
:
File: contracts/libraries/LiquidityPosition.sol 254: (amount0, amount1) = Base.UNI_POSITION_MANAGER.decreaseLiquidity( 255: INonfungiblePositionManager.DecreaseLiquidityParams({ ... // ... other params 260: deadline: block.timestamp 261: }) 262: );
As you see above, all liquidity interactions done with uniswap are done without any deadline set. These are called through ParticlePositionManager::increaseLiquidity
, decreaseLiquidity
, mint
, openPosition
, closePosition
and liqidatePosition
. Which makes any of these calls vulnerable to late inclusion by validators.
Manual audit
Consider adding a deadline
parameter to all of the AMM interactions through ParticlePositionManager::increaseLiquidity
, decreaseLiquidity
, mint
, openPosition
, closePosition
and liqidatePosition
as all these do interactions with uniswap.
Timing
#0 - c4-judge
2023-12-21T22:07:39Z
0xleastwood marked the issue as duplicate of #59
#1 - c4-judge
2023-12-23T22:37:31Z
0xleastwood marked the issue as satisfactory
Data not available
https://github.com/code-423n4/2023-12-particle/blob/main/contracts/protocol/ParticlePositionManager.sol#L365 https://github.com/code-423n4/2023-12-particle/blob/main/contracts/protocol/ParticlePositionManager.sol#L581-L585
If the protocol updates the loan terms, this will affect existing loans taken under different terms.
LOAN_TERM
is a guarantee for the lender (liquidity provider) to eventually get their liquidity back. After loan term has passed the lender can cause the loan to be possible to liquidate by claiming their liquidity.
In a similar fashion the borrower is guaranteed (as long as they keep their premium up) to not be liquidated during the LOAN_TERM
.
This is enforced in ParticlePositionManager::liquidatePosition
:
File: contracts/protocol/ParticlePositionManager.sol 365: lien.startTime + LOAN_TERM < block.timestamp))
Since only the startTime
of the lien
is stored, the LOAN_TERM
is read at time of liquidation.
The protocol can change loan term. This would break all the previous agreements between lenders and borrowers. Which could possibly cause unwanted liquidations for borrowers or lenders to be forced to wait longer to get their liquidity back.
Manual audit
Consider storing the loan term in the lien
.
Other
#0 - c4-judge
2023-12-21T22:04:17Z
0xleastwood marked the issue as duplicate of #52
#1 - c4-judge
2023-12-23T22:40:54Z
0xleastwood marked the issue as satisfactory
Data not available
marginTo/From
is a way to both cover your position and increase your premium when opening a position. There is however a unintended limit on how much marginTo
you can provide when opening a position.
When doing the swap to increase leverage, the amountToMinimum
(minimum received amount) is calculated:
ParticlePositionManager::openPosition
:
File: contracts/protocol/ParticlePositionManager.sol 212: collateralTo - cache.amountToBorrowed - params.marginTo, // amount needed to meet requirement
As you see here, a marginTo > collateralTo - cache.amountToBorrowed
will underflow this calculation. Thus there is no way to provide a bigger margin than this.
A user cannot supply more margin than collateralTo - amountToBorrowed
before the opening of the position reverts.
There is a workaround to supply no marginTo
and then use addPremium
to increase the premium but I believe this issue still breaks the intended purpose of marginTo
in the openPosition
call.
Test in OpenPosition.t.sol
:
function testOpenLongPositionTooMuchMargin() public { // test based on `testBaseOpenLongPosition` and `_borrowToLong` uint128 borrowerLiquidity = _liquidity / _borrowerLiquidityPorition; (uint256 amount0ToBorrow, uint256 amount1ToBorrow) = LiquidityAmounts.getAmountsForLiquidity( _sqrtRatioX96, _sqrtRatioAX96, _sqrtRatioBX96, borrowerLiquidity ); (, uint256 requiredEth) = particleInfoReader.getRequiredCollateral(borrowerLiquidity, _tickLower, _tickUpper); uint256 amountNeeded = QUOTER.quoteExactOutputSingle( address(USDC), address(WETH), FEE, requiredEth - amount1ToBorrow, 0 ); uint256 amountFrom = amountNeeded + amountNeeded / 1e6 - amount0ToBorrow; uint256 amountSwap = amountFrom + amount0ToBorrow; amountFrom += (amountSwap * FEE_FACTOR) / (BASIS_POINT - FEE_FACTOR); // user supplies a large `marginTo` to get a big premium uint256 marginTo = requiredEth - amount1ToBorrow + 1; vm.startPrank(WHALE); USDC.transfer(SWAPPER, amountFrom); WETH.transfer(SWAPPER, marginTo); vm.stopPrank(); vm.startPrank(SWAPPER); TransferHelper.safeApprove(address(USDC), address(particlePositionManager), amountFrom); TransferHelper.safeApprove(address(WETH), address(particlePositionManager), marginTo); // impossible as it will revert on underflow vm.expectRevert(stdError.arithmeticError); particlePositionManager.openPosition( DataStruct.OpenPositionParams({ tokenId: _tokenId, marginFrom: amountFrom, marginTo: marginTo, amountSwap: amountSwap, liquidity: borrowerLiquidity, tokenFromPremiumPortionMin: 0, tokenToPremiumPortionMin: 0, marginPremiumRatio: type(uint8).max, zeroForOne: true, data: new bytes(0) // will not make it to the swap }) ); vm.stopPrank(); }
Manual audit
Consider not counting marginTo
towards expected output from the swap. As shown in another issue, there are further problems with this. marginTo
is the premium for the "to"-side of the position. Hence should not be part of the expected output of the swap as it is the safety for the position.
Under/Overflow
#0 - c4-judge
2023-12-21T22:02:23Z
0xleastwood marked the issue as primary issue
#1 - wukong-particle
2023-12-23T06:14:18Z
Oh this is very interesting and careful point! So instead of the current check, we can do
params.marginTo > collateralTo - cache.amountToBorrowed ? 0 : collateralTo - cache.amountToBorrowed - params.marginTo
we still need marginTo to make the minimum amount for the swap right, happy to go back to the figures in https://excalidraw.com/#json=TcmwLn2W4K9H_UlCExFXa,J_yKjXNaowF0gYL8uvPruA for further discussion
#2 - c4-judge
2023-12-23T23:02:15Z
0xleastwood marked the issue as selected for report
#3 - wukong-particle
2023-12-24T09:36:21Z
confirm with the issue but our intended fix is slightly different from suggested.
#4 - c4-sponsor
2023-12-24T09:36:27Z
wukong-particle (sponsor) confirmed
#5 - romeroadrian
2023-12-29T14:27:26Z
Good catch 👍
Data not available
When a liquidity provider wants to withdraw their liquidity they can call ParticlePositionManager::reclaimLiquidity
. This will prevent any renewals:
ParticlePositionManager::addPremium
:
File: protocol/ParticlePositionManager.sol 508: // check LP allows extension of this lien 509: if (lps.getRenewalCutoffTime(lien.tokenId) > lien.startTime) revert Errors.RenewalDisabled();
getRenewalCutoffTime
is however never checked when opening a new position.
Hence a borrower can instead of renewing by adding premium simply close their position and open a new one. Since this opens a new loan with a new lien.startTime
there's nothing the liquidity provider can do to prevent this.
A borrower can prevent a liquidity provider from withdrawing their liquidity by closing and opening a new position.
PoC test, can be added to ClosePosition.t.sol
:
function testStopWithdrawLiquidity() public { _openLongPosition(); // LP decides to withdraw their liquidity vm.prank(LP); particlePositionManager.reclaimLiquidity(_tokenId); vm.warp(block.timestamp + LOAN_TERM - 1); // borrower doesn't want that and can continue borrowing liquidity // by closing then opening a new position against same LP position _closeLongPosition(0, true, true); _openLongPosition(); // 1 second after loan term vm.warp(block.timestamp + 2); // LP cannot claim all of their liquidity vm.prank(LP); vm.expectRevert(); particlePositionManager.decreaseLiquidity(_tokenId, _liquidity); }
Manual audit
Consider blocking openPosition
if renewalCutoffTime
is set to allow a liquidity provider to completely withdraw.
DoS
#0 - c4-judge
2023-12-21T22:09:44Z
0xleastwood marked the issue as primary issue
#1 - c4-judge
2023-12-21T22:11:24Z
0xleastwood marked the issue as duplicate of #35
#2 - c4-judge
2023-12-23T23:14:01Z
0xleastwood marked the issue as satisfactory
Data not available
Premium in ParticlePositionManager
is used to cover trading fees accrued for the liquidity borrowed. When liquidating, a portion of the premium is also used for the liquidation reward.
The issue is that a borrower can open a position without any premium at all:
ParticlePositionManager::openPosition
:
File: contracts/protocol/ParticlePositionManager.sol 240: if ( 241: cache.token0PremiumPortion < params.tokenToPremiumPortionMin || 242: cache.token1PremiumPortion < params.tokenFromPremiumPortionMin 243: ) revert Errors.InsufficientPremium();
params.tokenFrom/ToPremiumPortionMin
are supplied by the borrower hence can be 0
.
This removes any liquidation reward, since closeCache.tokenFrom/ToPremium
is 0
:
ParticlePositionManager::liquidatePosition
:
File: contracts/protocol/ParticlePositionManager.sol 349: liquidateCache.liquidationRewardFrom = 350: ((closeCache.tokenFromPremium) * LIQUIDATION_REWARD_FACTOR) / 351: uint128(Base.BASIS_POINT); 352: liquidateCache.liquidationRewardTo = 353: ((closeCache.tokenToPremium) * LIQUIDATION_REWARD_FACTOR) / 354: uint128(Base.BASIS_POINT);
Thus any liquidation incentives are removed. Even if the position would immediately be liquidatable there would be no incentive to liquidate it. Other than possibly for the LP as they would get their liquidity back.
0
premium also robs the liquidity provider of any fees that would have been accrued for their borrowed liquidity:
ParticlePositionManager::_closePosition
(same for the other branch of zeroForOne
):
File: contracts/protocol/ParticlePositionManager.sol 461: cache.token0Owed = cache.token0Owed < cache.tokenToPremium ? cache.token0Owed : cache.tokenToPremium; 462: cache.token1Owed = cache.token1Owed < cache.tokenFromPremium ? cache.token1Owed : cache.tokenFromPremium;
Here, min(tokenOwed,tokenPremium)
is taken, since tokenPremium
would be 0
no fees would be owed.
A borrower can open a position without any premium. This removes any incentive to liquidate the position and also robs the liquidity provider of fees that would have been accrued.
In the extreme, if you for example open a long position above the price (thus with no leverage), a borrower will not need to provide any funds at all. Thus can spam positions like this locking liquidity providers liquidity and denying them fees (if the trading moves in their direction).
Actually, the basic test testBaseOpenLongPosition
in OpenPosition.t.sol
already reproduces this:
function testOpenPositionWithoutPremium() public { testBaseOpenLongPosition(); (,, uint128 token0Premium, uint128 token1Premium,,) = particleInfoReader.getOwedInfo(SWAPPER, 0); assertEq(0,token0Premium); assertEq(0,token1Premium); }
Manual audit
Consider implementing a minimum enforced premium portion. Any premium portion would guarantee a reward to the liquidator.
Invalid Validation
#0 - c4-judge
2023-12-21T21:42:07Z
0xleastwood marked the issue as primary issue
#1 - wukong-particle
2023-12-23T06:02:27Z
Good suggestion, will add a minimum premium parameter. Not agreeing with the severity though. In the worst case, the LP is still willing to close the position to reclaim liquidity. But will let the judge to ultimately decide. Thanks!
#2 - 0xleastwood
2023-12-23T19:16:38Z
This seems like a DoS on the LP's liquidity. It will still be possible for them to reclaim if they liquidate a position and subsequently withdraw their liquidity but this is still not ideal.
I guess my final question would be do trader's lose any funds when creating positions with no premium? Is there a financial disincentive to do this? This would be the difference between QA and medium severity imo. For the time-being I will downgrade to medium.
#3 - c4-judge
2023-12-23T19:16:48Z
0xleastwood changed the severity to 2 (Med Risk)
#4 - 0xleastwood
2023-12-23T19:18:24Z
@wukong-particle I forgot to tag you in the above comment.
#5 - wukong-particle
2023-12-24T09:34:51Z
Well, trader will lose their initial margin if the premium is 0 and got immediately liquidated. If the trader believes immediate liquidation won't happen (because there is no reward from premium), logically the trader will go with 0 premium because this would be the least amount of cost. We will add a minimum premium parameter here so we confirm the issue.
#6 - c4-sponsor
2023-12-24T09:34:56Z
wukong-particle (sponsor) confirmed
#7 - c4-judge
2023-12-24T12:34:24Z
0xleastwood marked the issue as selected for report
#8 - romeroadrian
2023-12-30T14:34:37Z
@0xleastwood Isn't this a duplicate of #27? I think both describe the same core issue with different examples/scenarios
#9 - c4-judge
2023-12-31T12:52:08Z
0xleastwood marked the issue as not selected for report
#10 - c4-judge
2023-12-31T12:52:18Z
0xleastwood marked the issue as duplicate of #27
#11 - 0xleastwood
2023-12-31T12:52:47Z
@0xleastwood Isn't this a duplicate of #27? I think both describe the same core issue with different examples/scenarios
correct, it should be a duplicate of #27. Thanks
#12 - c4-judge
2023-12-31T13:09:30Z
0xleastwood marked the issue as satisfactory
Data not available
https://github.com/code-423n4/2023-12-particle/blob/main/contracts/libraries/LiquidityPosition.sol#L190-L199 https://github.com/code-423n4/2023-12-particle/blob/main/contracts/libraries/LiquidityPosition.sol#L255-L261
Lack of slippage protection for increasing and decreasing liquidity can cause the liquidity provider to provide liquidity at an unfavorable price. Or the borrower to borrow/repay in a manipulated pool.
When adding liquidity eventually calls comes down to LiquidityPosition::increaseLiquidity
and decreaseLiquidity
which interact with the Uniswap position manager:
LiquidityPosition::increaseLiquidity
and decreaseLiquidity
:
File: contracts/libraries/LiquidityPosition.sol 178: function increaseLiquidity( // ... params 184: ) internal returns (uint128 liquidity, uint256 amount0Added, uint256 amount1Added) { // ... set approvals 189: // increase liquidity via position manager 190: (liquidity, amount0Added, amount1Added) = Base.UNI_POSITION_MANAGER.increaseLiquidity( 191: INonfungiblePositionManager.IncreaseLiquidityParams({ 192: tokenId: tokenId, 193: amount0Desired: amount0, 194: amount1Desired: amount1, // @audit no slippage for which price to add liquidity to 195: amount0Min: 0, 196: amount1Min: 0, 197: deadline: block.timestamp 198: }) 199: ); // ... reset approvals 204: } ... 253: function decreaseLiquidity(uint256 tokenId, uint128 liquidity) internal returns (uint256 amount0, uint256 amount1) { 254: (amount0, amount1) = Base.UNI_POSITION_MANAGER.decreaseLiquidity( 255: INonfungiblePositionManager.DecreaseLiquidityParams({ 256: tokenId: tokenId, 257: liquidity: liquidity, // @audit no slippage or how much tokens to get when decreasing liquidity 258: amount0Min: 0, 259: amount1Min: 0, 260: deadline: block.timestamp 261: }) 262: ); 263: }
These are called directly by the liquidity provider through: ParticlePositionManager::increaseLiquidity
and ParticlePositionManager::decreaseLiquidity
.
As well as indirectly when opening, closing or liquidating a position.
Manual audit
Consider adding amount0/1Min
parameters to ParticlePositionManager::increaseLiquidity
and decreaseLiquidity
and also through the calls openPosition
, closePosition
and liquidatePosition
.
This would also cover the usage of slot0
in Base::getRequiredRepay
as it would enforce a certain amount of token0/1
to be returned when repaying the liquidity.
MEV
#0 - c4-judge
2023-12-21T21:37:17Z
0xleastwood marked the issue as duplicate of #2
#1 - c4-judge
2023-12-23T23:01:16Z
0xleastwood marked the issue as satisfactory
Data not available
id | title |
---|---|
L-01 | ParticlePositionManager::onERC721Received allows all ERC721 tokens |
L-02 | ParticlePositionManager can be initialized with bad params |
L-03 | Fee tier 100 is also supported by Uniswap |
L-04 | Use of non-upgradable ReentrancyGuard |
L-05 | Some tokens revert on 0 amount transfer |
L-06 | Uniswap positions minted to ParticlePositionManager are stuck |
NC-01 | Liquidation criteria very complicated |
NC-02 | Confusing parameter naming in ParticlePositionManager::swap |
NC-03 | Erroneous comment |
ParticlePositionManager::onERC721Received
allows all ERC721 tokensParticlePositionManager::onERC721Received
:
File: contracts/protocol/ParticlePositionManager.sol 98: function onERC721Received( ... 103: ) external override returns (bytes4) { 104: if (msg.sender == Base.UNI_POSITION_MANAGER_ADDR) { // ... add liq position 109: } // all ERC721s are accepted 110: return this.onERC721Received.selector; 111: }
As you see above all ERC721 tokens are accepted. This can cause ERC721 tokens accidentally sent to the contract to be stuck as there is no way to transfer them back (apart from code upgrade).
Consider only allowing UNI_POSITION_MANAGER
to send ERC721 tokens to the contract and reverting otherwise.
ParticlePositionManager
can be initialized with bad paramsParticlePositionManager
has some parameters that are used to control fees, liquidation rewards and loan terms. If these params are incorrect they can cause severe problems with the contract. Therefore, when updating them, they are verified to be within certain limits. This is however not done, when initilizing the contract:
ParticlePositionManager::initialize
:
File: contracts/protocol/ParticlePositionManager.sol 60: function initialize( 61: address dexAggregator, 62: uint256 feeFactor, 63: uint128 liquidationRewardFactor, 64: uint256 loanTerm, 65: uint256 treasuryRate 66: ) external initializer { 67: __UUPSUpgradeable_init(); 68: __Ownable_init(); 69: DEX_AGGREGATOR = dexAggregator; 70: FEE_FACTOR = feeFactor; 71: LIQUIDATION_REWARD_FACTOR = liquidationRewardFactor; 72: LOAN_TERM = loanTerm; 73: _treasuryRate = treasuryRate; 74: }
Hence the contract can be setup with any values:
function testSetupWithBadParams() public { address impl = address(new ParticlePositionManager()); bytes memory init = abi.encodeWithSelector(ParticlePositionManager.initialize.selector, address(0), type(uint256).max, type(uint128).max, type(uint256).max, type(uint256).max); ParticlePositionManager ppm = ParticlePositionManager(address(new ERC1967Proxy(address(impl),init))); assertEq(address(0),ppm.DEX_AGGREGATOR()); assertEq(type(uint256).max,ppm.FEE_FACTOR()); assertEq(type(uint128).max,ppm.LIQUIDATION_REWARD_FACTOR()); assertEq(type(uint256).max,ppm.LOAN_TERM()); // treasury rate cannot be verified as it isn't public and there's no getter for it // assertEq(type(uint256).max,ppm._treasuryRate()); }
Consider using the setter functions already declared, update*(...)
, to initialize the contract parameters. That would guarantee that the initialization is done using the same validation as changing the parameters.
100
is also supported by UniswapThe helper contract ParticleInfoReader
has a function to view which uniswap pool has the deepest liquidity:
ParticleInfoReader::getDeepPool
:
File: contracts/protocol/ParticleInfoReader.sol 102: function getDeepPool(address token0, address token1) external view returns (address deepPool) { 103: uint24[3] memory feeTiers = [uint24(500), uint24(3000), uint24(10000)]; 104: uint128 maxLiquidity = 0; 105: 106: for (uint256 i = 0; i < feeTiers.length; i++) { 107: address poolAddress = Base.UNI_FACTORY.getPool(token0, token1, feeTiers[i]); 108: if (poolAddress != address(0)) { 109: IUniswapV3Pool pool = IUniswapV3Pool(poolAddress); 110: uint128 liquidity = pool.liquidity(); 111: if (liquidity > maxLiquidity) { 112: maxLiquidity = liquidity; 113: deepPool = poolAddress; 114: } 115: } 116: } 117: }
The issue is that uniswap supports more feeTiers
:
https://docs.uniswap.org/concepts/protocol/fees#pool-fees-tiers:
More fee levels may be added by UNI governance, e.g. the 0.01% fee level added by this governance proposal in November 2021, as executed here.
If you look at the contract:
https://etherscan.io/address/0x1f98431c8ad98523631ae4a59f267346ea31f984#readContract
it returns tick spacing 1
for fee tier 100
Hence this might not return the pool with the deepest liquidity.
Consider adding 100
to the list of fee tiers
ReentrancyGuard
ParticlePositionManager
is an upgradeable contract. Therefore it uses both Ownable2StepUpgradeable
and UUPSUpgradeable
from OZ. It does not however use ReentrancyGuardUpgradeable
but instead ReentrancyGuard
:
ParticlePositionManager.sol#L19-L26
:
File: contracts/protocol/ParticlePositionManager.sol 19: contract ParticlePositionManager is 20: IParticlePositionManager, 21: Ownable2StepUpgradeable, 22: UUPSUpgradeable, 23: IERC721Receiver, 24: ReentrancyGuard, 25: Multicall 26: {
If ReentrancyGuard
was upgraded in the future with new state this could cause issues for ParticlePositionManager
.
Consider using ReentrancyGuardUpgradeable
from OZ.
0
amount transferParticlePositionManager::liquidatePosition
:
File: protocol/ParticlePositionManager.sol 376: // reward liquidator 377: TransferHelper.safeTransfer(closeCache.tokenFrom, msg.sender, liquidateCache.liquidationRewardFrom); 378: TransferHelper.safeTransfer(closeCache.tokenTo, msg.sender, liquidateCache.liquidationRewardTo);
Some tokens, like LEND
revert on 0
amount transfer. If the liquidation amount is 0
for this token this would make liquidation impossible for this position.
Consider checking if the liquidationAmount > 0
before transferring liquidation reward.
ParticlePositionManager
are stuckUsing the NonfungiblePositionManager
directly a user could potentially mint a position directly to ParticlePositionManager
by having ParticlePositionManager
as the receiver
when calling NonfungiblePositionManager::mint
.
Since NonfungiblePositionManager
doesn't use safeMint
this would just transfer the token and liquidity to ParticlePositionManager
without calling ParticlePositionManager::onERC721Received
, thus the liquidity would be lost.
Consider adding a rescue function callable by owner
that can transfer any accidentally minted tokens out of the contract. This could check that ParticlePositionManager
is the owner of the token but has lps.owner == address(0)
to prevent misuse.
ParticlePositionManager::liquidatePosition
:
File: contracts/protocol/ParticlePositionManager.sol 358: // check for liquidation condition 359: ///@dev the liquidation condition is that 360: /// (EITHER premium is not enough) OR (cutOffTime > startTime AND currentTime > startTime + LOAN_TERM) 361: if ( 362: !((closeCache.tokenFromPremium < liquidateCache.tokenFromOwed || 363: closeCache.tokenToPremium < liquidateCache.tokenToOwed) || 364: (lien.startTime < lps.getRenewalCutoffTime(lien.tokenId) && 365: lien.startTime + LOAN_TERM < block.timestamp)) 366: ) { 367: revert Errors.LiquidationNotMet(); 368: }
Even with the comment it is very hard to understand this criteria.
I suggest to move it to a function canBeLiquidated
or similar where the logic can be broken up into multiple blocks and made easier to comprehend.
ParticlePositionManager::swap
It's called token0
/1
but later in the swap library its tokenFrom
/To
.
The parameters would be easier to understand if they were from/to in ParticlePositionManager::swap
as well.
ParticlePositionManager::_closePosition
:
File: contracts/protocol/ParticlePositionManager.sol 422: // add liquidity back to borrower
borrower
is not correct here, liquidity is added back to the lender/liquidity provider.
#0 - c4-judge
2023-12-26T11:23:43Z
0xleastwood marked the issue as selected for report
#1 - c4-judge
2023-12-26T11:23:46Z
0xleastwood marked the issue as grade-a
#2 - c4-sponsor
2023-12-29T04:12:21Z
wukong-particle (sponsor) confirmed
#3 - 0xleastwood
2023-12-30T12:09:09Z
I consider all these issues to be valid.