Poolshark engaged Guardian to review the security of their Directional AMM Limit Pool. From the 18th of July to the 18th of August, a team of 6 security researchers reviewed the source code in scope. The auditing approach championed manual analysis to uncover novel exploits and verify intended behavior with supporting verification from fuzzing with Echidna. All invariants, findings, and remediations have been recorded in the following report.
- Published
- Review window
- July 18 to August 18, 2023
- Language
- Solidity
- Chains
- Arbitrum, Scroll
- Sector
- DEXs and AMMs
- 14 Critical
- 10 High
- 12 Medium
- 39 Low
- 0 Informational
Scope
Overview
Poolshark engaged Guardian to review the security of their Directional AMM Limit Pool. From the 18th of July to the 18th of August, a team of 6 security researchers reviewed the source code in scope. The auditing approach championed manual analysis to uncover novel exploits and verify intended behavior with supporting verification from fuzzing with Echidna. All invariants, findings, and remediations have been recorded in the following report.
Issues Detected Throughout the course of the audit numerous high impact issues were uncovered and promptly remediated by the Poolshark team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the functionality described for the Directional AMM Limit Pool.
Code Quality From the 18th of July to the 18th of August, the codebase quality improved considerably. However, it is recommended to improve in-code documentation supporting NatSpec standards and to update the Poolshark Whitepaper. Additionally, given the scope of changes made to the codebase and number of critical issues detected, Guardian supports an independent security review of the protocol at a finalized frozen commit.
Findings 75
-
GLOBAL-1 Critical Shared TickMap Errantly Unsets Ticks Logical Error Resolved
Description
Proof of concept: PoC
When burning a
zeroForOneposition, ticks in theTickMapare unset deAcknowledged on if there is aliquidityDeltaof 0 at that tick in theticks0mapping. However, there may be a nonzeroliquidityDeltaon that tick in theticks1mapping.This leads to ticks being unset from one
zeroForOneside when they are critical for the other. Therefore these ticks will not be crossed during a swap which invalidates the protocol accounting.Recommendation
Adopt two
TickMapsandEpochMapsto solve this particular failure case as well as to avoid any additional potential logical errors related to sharing aTickMapandEpochMap.Resolution
Poolshark Team: The issue was resolved in commit 0e40b90.
-
CLAIMS-1 Critical Unset End Tick Allows Malicious Claims Logical Error Resolved
Description
Proof of concept: PoC
During a swap a position’s end tick may become unset, leaving a position with no upper tick that is set in the
TickMap. The pool price can then be undercut such that price is below the position’s end tick.The user is then able to claim at the current pool price and pass the claims validation as the
TickMap.nextyields the max tick which carries an unsetepochLast.Ultimately this allows the user to withdraw
token0andtoken1balances from the contract that do not correspond to their actual fill amount, invalidating the system's accounting.Recommendation
Always validate the position’s end tick
epochLastin the claims validation logic with the following:uint32 endTickAccumEpoch = EpochMap.get(params.zeroForOne ? params.upper : params.lower, tickMap, constants); if (endTickAccumEpoch > cache.position.epochLast) { require (false, 'WrongTickClaimedAt5()'); }Resolution
Poolshark Team: The issue was resolved in commit 0e40b90.
-
MCALL-1 Critical Pool State Unsaved Leading To Underflow Underflow Resolved
Description
Proof of concept: PoC
The
poolstate is only saved if a position is minted, e.g. ifparams.amount > 0 && params.lower <params.upperis satisfied. However, liquidity for thepoolcan be unlocked without a position actually being minted when theparams.amountis swapped entirely and the below case is entered.if (cache.pool.liquidity == 0) { /// @dev - this makes sure to have liquidity unlocked if undercutting (cache, cache.pool) = Ticks.unlock(cache, cache.pool, ticks, tickMap, params.zeroForOne); }As a result, liquidity can be unlocked in the pool yet never saved. Once the liquidity is unlocked, the tick where the liquidity was stashed is cleared. This leads to catastrophic consequences as the
liquidityDeltaon the cleared tick will not be crossed and added to the pool.Ultimately, there will be liquidity underflow when the corresponding negative
liquidityDeltais added to thepool.liquidityduringTicks.unlockorTicks._cross. This breaks a key invariant thatpool.liquidityshould never underflow, disrupting allLimitPooloperations.Recommendation
Save the pool state even when a position is not minted in
MintCall.perform.Resolution
Poolshark Team: The recommendation was implemented in commit 00db8d3.
-
BCALL-1 Critical Overwritten Position On Remove Logical Error Resolved
Description
Proof of concept: PoC
When the
Positions.removefunction is called, the updatedcache.positionis not returned.Therefore at the end of the
performfunction all updates are overwritten.if ((params.zeroForOne ? params.claim != params.upper : params.claim != params.lower)) params.zeroForOne ? positions[msg.sender][params.claim][params.upper] = cache.position : positions[msg.sender][params.lower][params.claim] = cache.position;Consequently, this duplicates the amounts of a position, leaving liquidity in the position when there should not be any since the state of the position prior to the
burnis recorded.Recommendation
Return the updated
cache.positionfromPositions.removeand save this updated position state.Resolution
Poolshark Team: The recommendation was implemented in commit 5a30bbf.
-
CLAIMS-2 Critical Pool Bricked Due To Null Position Logical Error Resolved
Description
Proof of concept: PoC
When a position has been fully filled and a user is claiming with a
params.amountof 0 the position is resized to a range spanning 0 ticks, e.g. [100, 100], however the position remains at those ticks with a nonzero liquidity.The user is now able to burn this remaining null position with a nonzero
params.amount, however now theparams.lower == params.upper == params.claim.Therefore for
zeroForOnepositions, whencache.pool.price < cache.priceLower, thecache.removeLowerwill be assigned totrue.And for
!zeroForOnepositions, whencache.pool.price > cache.priceLower, cache.removeUpperwill be assigned totrue.This results in the position’s liquidity being subtracted from the start tick, when the liquidity had already previously been removed when the position was originally crossed into and filled.
Therefore the
startTick.liquidityDeltawill be more negative than thepool.liquidityat that price and result in the entire pool being bricked upon reaching thestartTick.Recommendation
When users are claiming a fully filled position where the
params.claim == endTick, always zero out the position liquidity among other attributes as the position no longer exists.Resolution
Poolshark Team: The recommendation was implemented in commit 3682ae9.
-
TK-1 Critical Pool Liquidity Double Counted Logical Error Resolved
Description
Proof of concept: PoC
In the
Ticks.insertSinglefunction, thepool.liquidityis often zeroed out and stashed onto thetickToSave. However in some cases thepool.liquiditywill not be zeroed out, and yet it is still stashed on thetickToSave.This means that the
pool.liquidityis immediately double counted in both the active liquidity and thetick.liquidityDelta. Now if this tick is crossed thisliquidityDeltawill be added to thepool.liquidityand the pool will have double the liquidity than it ought to at the current price.Therefore users will be able to swap using more liquidity than exists at the current range, and they will draw from tokens in positions that are far away from the current pool price. This is catastrophic, as users will not be able to burn their positions as their underlying token amounts have been improperly swapped at the current price, leaving a lack of
tokenInto withdraw.Recommendation
Ensure that whenever the
liquidityDeltais updated on thetickToSave, thepool.liquidityis zeroed out, every time. Include the zeroing out of thepool.liquidityexactly where thetick.liquidityDeltais incremented on line 490.Resolution
Poolshark Team: The recommendation was implemented in commit d5443ff.
-
PROU-1 Critical Stolen Approvals Access Control Resolved
Description
The
poolsharkSwapCallbackfunction on thePoolRoutercontract does not validate that themsg.senderis a real Poolshark limit pool and yet gives themsg.senderthe ability to transfer from any address to themsg.sender.Therefore any user that has approved the
PoolRoutercontract may have their approval amounts stolen by an arbitrary address invoking thepoolsharkSwapCallback.Recommendation
Validate that the
msg.senderis indeed a registered PoolsharkLimitPoolupon invocation of thepoolsharkSwapCallbackfunction.Resolution
Poolshark Team: The recommendation was implemented in commit 593ff6a.
-
TMAP-1 Critical Broken Swap Due To Incorrect Cross Tick Logical Error Resolved
Description
Proof of concept: PoC
Because the
TickMapcan round up when performingtick += tickSpacing / 2, there is potential for thecrossTickto be set to the current pool price rather than the next inserted tick when a position is minted. Thus, when a trader attempts to swap, the swap is performed from the current price to the cross price which are equivalent, leading to no swap at all although liquidity is available. This goes against the core functionality of the pool where a swap should be able to be performed when liquidity is available and users can get a fill.Consider the following scenario where LPs mint
oneForZero:- Bob mints a position with ticks [-100, 100] which shifts the price to tick 100.
- Alice mints a position with ticks [-100, 100] which calls
insertSingleon tick 100 since liquidity is
now available in the pool due to Bob’s mint.
- Swapper comes in to swap and the cross tick is calculated to be
TickMap.previous(tickMap,
pool.tickAtPrice, cache.constants.tickSpacing, true)withtickAtPricebeing 100.- Due to
roundUpbeingtrue, the tick inTicks.previousbecomes tick 105. The previous tick from tick
105 is now tick 100.
- Because the
crossTickis tick 100 rather than -100, no swap occurs as no ticks are crossed.
Recommendation
Do not round up when calculating the
crossTick.Resolution
Poolshark Team: The issue was resolved in commit 27c03bb.
-
TK-2 Critical Liquidity Underflow Due To Tick Rounding Rounding Resolved
Description
Proof of concept: PoC
During
MintCall.perform, liquidity is unlocked in the swap pool in the case there is no liquidity. When thepool.tickAtPriceis negative, the tick rounds up when fetching theTickMap.next()tick. However rounding up allows the pool to skip over a tick with nonzeroliquidityDelta.Consequently, the
ticks[pool.tickAtPrice].liquidityDeltacan be negative with larger magnitude than the current liquidity, which will lead to silent underflow when casting theint128value to auint128valueuint128(ticks[pool.tickAtPrice].liquidityDelta).As a result, the liquidity of the swap pool becomes severely inflated due to underflow which leads to significant loss of funds for users as traders are able to swap near infinite amounts at the current pool price.
Recommendation
Round back in
TickMap.next()when the tick is negative.Resolution
Poolshark Team: The recommendation was implemented in commit 614db68.
-
TK-3 Critical Cross Tick Skips Half Ticks Logical Error Resolved
Description
Proof of concept: PoC
It is possible to enter the case
if (cache.amountLeft < amountMax)during a swap, but have thenewPricebe represented by a tick that is lower than thecache.crossTick. The tick of thenewPricecan be less than thecache.crossTickbecause whenTicks.insertSingleis called, thepriceAtattribute is set onto a rounded tick for the current pool price, e.g. current pool price at tick 4 is saved on the rounded tick 5.Consequently, the tick at the
newPricemay jump over a tick where liquidity is stashed(liquidityDelta> 0)during a swap. Because no cross is performed when insideif (cache.amountLeft < amountMax), the liquidity is never activated.Since the activated pool liquidity is smaller than it should be,
pool.liquidityunderflows when a future cross occurs and a tick withliquidityDelta < 0is crossed. This breaks a key invariant thatpool.liquidityshould never underflow, disrupting a allLimitPooloperations.Recommendation
In the case that the tick at the
newPriceis smaller/larger than thecache.crossTick(jumped over a tick where liquidity is stashed) in thezeroForOne/oneForZerocase, setcross = Trueso that the stashed liquidity delta does get activated.Resolution
Poolshark Team: The recommendation was implemented in commit 89cce40.
-
LMP-1 Critical Lack Of Access Restriction For Initialize Function Access Control Resolved
Description
The
initializefunction on theLimitPoolhas unrestricted access and can be called after initialization has already occurred.Recommendation
Add an
onlyInitializermodifier to the initialize function so that the it cannot be called after initialization.Resolution
Poolshark Team: The recommendation was implemented in commit 80501d6.
-
TK-4 Critical Users Can Maliciously Claim At The Current Pool Price Logical Error Resolved
Description
Proof of concept: PoC
When a new tick is inserted in
Positions.addduring a position mint the new tick is initialized with an epoch of 0. However the claim validation logic inClaims.validaterelies on the epoch of the next initialized tick to determine whether or not the user should be able to claim at the supplied claim tick.When a new position is created and a new upper and/or lower tick is created, the new tick(s) will invalidate the
Claims.validatelogic as any user can now claim at a tick that is directly previous to a newly initialized tick.Therefore users are able to claim at the current pool price even when it is not the furthest claim tick they should be claiming at. When a user claims at the current pool price and burns liquidity, that liquidity will be removed from the
pool.liquidityvalue.However a user’s position can have their liquidity stashed at a higher tick upon undercutting and therefore not have active liquidity when they are claiming at the current pool price.
This leads to users removing liquidity from the active
pool.liquiditythat should not have been and invalidates the pool accounting system leading to locked positions among other catastrophic consequences.Recommendation
When a new tick is inserted during
Positions.add, do not initialize it with an epoch of 0. Instead initialize new ticks with the same epoch as the tick further along in thezeroForOneor!zeroForOnedirection.Resolution
Poolshark Team: The recommendation was implemented in commit 8a6c3bf.
-
TK-5 Critical Half Tick Liquidity Never Unlocked Logical Error Resolved
Description
Proof of concept: PoC
When an undercut is performed, the pool’s liquidity is stashed on a half tick with the current price.
It is crucial when swapping that this stashed liquidity is kicked into the pool, otherwise the negative
liquidityDeltaon the end tick of a position will exceed thepool.liquidity, causing underflow. However, it is possible for an iteration of a swap to skip a half tick where liquidity is stashed as thecache.crossTickcan jump to thelimitTickAheadin_iterate().For example, the
cache.crossTickmay equal 5 after:(cache.crossTick,) = TickMap.roundHalf(cache.crossTick, cache.constants, cache.price);But the
limitTickAheadmay be 0 after:int24 limitTickAhead = TickMap.previous(limitTickMap, cache.crossTick, cache.constants.tickSpacing, inclusive); cache.crossTick = limitTickAhead;Therefore, the tick 5 stashed liquidity is never activated since it isn’t crossed.
Recommendation
Add the liquidity on the half tick before going to the tick ahead and then clear the tick’s liquidity delta.
Resolution
Poolshark Team: The recommendation was implemented in commit b633427.
-
TK-6 Critical pool.price Not Updated In Ticks.unlock Logical Error Resolved
Description
Proof of concept: PoC
In the
Ticks.unlockfunction, theticks[pool.tickAtPrice]is zeroed out before the following logic is executed to update thepool.price:uint160 priceAt = ticks[pool.tickAtPrice].priceAt; if (priceAt > 0) { pool.price = priceAt; pool.tickAtPrice = ConstantProduct.getTickAtPrice(priceAt, cache.constants); }Since the
ticks[pool.tickAtPrice]is always zeroed out before this logic, theif (priceAt > 0)case will never be entered and thepool.pricewill never be updated. As a result, thepool.tickAtPricewill not agree with thepool.price. Ultimately this will cause theamountMaxto be significantly larger than it should be in thequoteSinglecall during a swap, as it relies on thepool.priceand thecrossPricewhich relies on thetickAtPrice.Recommendation
Zero out the
ticks[pool.tickAtPrice]after performing thepool.priceupdate logic which depends on theticks[pool.tickAtPrice].Resolution
Poolshark Team: The recommendation was implemented in commit 1bf9eaa.
-
TK-7 High Swaps Bricked Due To Malicious Position Logical Error Resolved
Description
In the
quoteSinglefunction, a swap cannot occur if thepool.pricebecomescache.constants.bounds.minorcache.constants.bounds.max. Therefore a user can create azeroForOneposition with minimal liquidity and a lower tick ofcache.constants.bounds.minto completely brick the pool and halt all!zeroForOneswaps from occurring.This renders the pool useless, an attacker can exercise this on every Poolshark’s
LimitPoolto completely shut down the protocol. Pools of the same tokens andtickSpacingcannot be re-deployed as they are already registered under the same key.Recommendation
Check the
pool.priceagainst thecache.constants.bounds.minandcache.constants.bounds.maxdependent on the swap direction. If a swap iszeroForOne, the swap should early return if the price iscache.constants.bounds.minand if a swap is!zeroForOne, the swap should early return if the price iscache.constants.bounds.max.Resolution
Poolshark Team: The recommendation was implemented in commit 5dc6b2b.
-
CLAIMS-3 High Position Resized To Half Tick Logical Error Resolved
Description
Proof of concept: PoC
When a user claims for their partially filled position, they are able to claim at a half tick that is not an even multiple of their
tickSpacing. This allows users to claim fills dependent on the stashedpriceAton the half tick, however it also results in the position getting resized to a start tick that happens to be that half tick.Positions with a half tick as a boundary break a fundamental invariant of the protocol and potentially lead to severe issues and manipulation.
Recommendation
Do not resize positions to the boundary of a half tick, instead round the new boundary tick back to the previous full tick.
Resolution
Poolshark Team: The recommendation was implemented in commit 0e40b90.
-
LMP-2 High Unclaimable Fees Logical Error Resolved
Description
The
feesfunction never setstoken0Feesandtoken1Feesvariables. Therefore, the owner will never be able to collect fees.Recommendation
Set
token0Feesandtoken1Feesprior to zeroing out the protocol fees.Resolution
Poolshark Team: The recommendation was implemented in commit b7ebe31.
-
TMAP-2 High Incorrect Tick Rounding Rounding Resolved
Description
In the
roundAheadWithPricefunction, ifzeroForOneand theroundedTickis negative, thetickSpacingis subtracted from the rounded tick. Otherwise if!zeroForOneand theroundedTickis positive, thetickSpacingis added to the rounded tick. Therefore for thezeroForOnecase a positiveroundedTickis rounded down and not adjusted upwards, meanwhile a negativeroundedTickis adjusted to be more negative. Both of these are rounding back rather than rounding ahead for azeroForOneposition. For the!zeroForOnecase a positiveroundedTickis adjusted upwards, meanwhile a negativeroundedTickis rounded to be less negative and is not adjusted to be more negative. Both of these are rounding back rather than rounding ahead for a!zeroForOneposition. Ultimately this results in the beginning of a position being resized to theroundedBacktick rather than theroundedAheadtick which unexpectedly alters the user’s overall execution price and unexpectedly sets the latestswapEpochon theroundedBacktick. Additionally, theroundAheadfunction implements incorrect tick rounding where forzeroForOnecases where theroundedTickis negative it is rounded up twice. First the magnitude of the negative tick is reduced with rounding and then it is further adjusted up by thetickSpacing. On the other hand, positiveroundedTicksare rounded down and not adjusted up. The inverses are true for the!zeroForOnecase.Recommendation
Use the following cases to accurately round ahead:
if (zeroForOne && (roundedTick > 0 || (roundedTick == 0 && tick > 0))) roundedTick += tickSpacing; else if (!zeroForOne && (roundedTick < 0 || (roundedTick == 0 && tick < 0))) roundedTick -= tickSpacing;Resolution
Poolshark Team: The recommendation was implemented in commit 4666f91.
-
GLOBAL-2 High Odd Tick Spacing Should Not Be Used Configuration Resolved
Description
Proof of concept: PoC
When an odd tick spacing is used in a
LimitPool, tick rounding errors can cause swaps to have access to more liquidity at the current market price than they should.Specifically in the
TickMap.previousorTickMap.nextfunctions the previous or next tick may be errantly rounded in the_tickfunction such that it yields a tick that was never set in theTickMap. This affects many areas of the protocol but invalidates the accounting system and leads to direct loss of funds when swappers are able to swap for an extra tick length using the same liquidity.Recommendation
Do not allow an odd
tickSpacingto be used as it is incompatible with the system.Resolution
Poolshark Team: Odd tick spacings are now disallowed in the
LimitPoolManagercontract. -
POS-1 High Yield Can Be Stolen From Liquidity Providers Logical Error Resolved
Description
Proof of concept: PoC
When computing the value of fees for a position the
priceLowerorpriceUpperis used as thecurrentPrice, however thepriceLowerorpriceUpperwill rarely be accurate to the current price.cache.liquidityOnPosition = ConstantProduct.getLiquidityForAmounts( cache.priceLower, cache.priceUpper, position.amount0 > 0 ? cache.priceLower : cache.priceUpper, position.amount1, position.amount0 )A malicious actor can leverage this inaccuracy to mint a position where the fees are undervalued from the
liquidityOnPositioncalculation and subsequently burn to receive the full value of the fees with the calculation in theremovefunction.params.amount = uint128(uint256(params.amount) * cache.totalSupply / (uint256(position.liquidity - params.amount) + cache.liquidityOnPosition)); /// @dev - if there are fees on the position we mint less positionTokenRecommendation
Convert the position accounting logic to an ERC721 implementation rather than an ERC1155 implementation to avoid unnecessary complexity with fee valuation and potential manipulation.
Resolution
Poolshark Team: The recommendation was implemented in commit 3356f37.
-
POS-2 High liquidityGlobal Not Decremented Logical Error Resolved
Description
When
params.amount == 0thepool.liquidityGlobalis decremented when theparams.claim !=params.lowerandparams.claim == params.lowerforzeroForOneand when theparams.claim !=params.upperandparams.claim == params.upperfor!zeroForOne.These conditions are unsatisfiable, therefore the
pool.liquidityGlobalwill not be decremented for positions that are fully filled and ought to have their liquidity removed frompool.liquidityGlobal. This way an attacker can continuously open positions and remove them until thepool.liquidityGlobalreaches the maximum and users are unable to mint positions rendering the pool useless.Recommendation
Appropriately decrement the
pool.liquidityGlobalwhen users are claiming at their end tick withparams.amount == 0.Resolution
Poolshark Team: The recommendation was implemented in commit f0d52ad.
-
CLAIMS-4 High Position Overwritten At Claim Tick Logical Error Resolved
Description
Proof of concept: PoC
It is possible for a user’s position to get overwritten at the claim tick because the
positionsmapping is accessed with the wrong ticks when performing claim tick validation.For a
zeroForOneposition, the new position should span from the claim tick to the upper tick. For a!zeroForOneposition, the new position should span from the lower tick to the claim tick. However, that is not how the validation is checking the user’s position.// prevent position overwriting at claim tick if (params.zeroForOne) { if (positions[params.owner][params.lower][params.claim].liquidity > 0) { require (false, string.concat('UpdatePositionFirstAt(', String.from(params.lower), ', ', String.from(params.claim), ')')); } } else { if (positions[params.owner][params.claim][params.upper].liquidity > 0) { require (false, string.concat('UpdatePositionFirstAt(', String.from(params.lower), ', ', String.from(params.claim), ')')); } }This can lead to a trader losing their funds, because any deltas for another position they have may be overwritten when burning one of their positions.
Recommendation
Modify the validation such that
zeroForOnechecks the position spanning from theparams.claimto theparams.upperand!zeroForOnechecks the position spanning from theparams.lowerto theparams.claim, which will prevent a claim tick that leads to an overwritten position.Resolution
Poolshark Team: The recommendation was implemented in commit c9a3a42.
-
CLAIMS-5 High Users Prevented From Burning Logical Error Resolved
Description
When a user's position is undercut and liquidity is stashed on a half tick, they are required to claim at that half tick. However the initial
priceClaimfor a half tick is assigned to the price at that half tick rather than thepriceAtfor the half tick. When thepool.priceis ahead of thepriceClaimthen theparams.claimwill be set to the earlier full tick.However this tick is not a valid tick to claim at for the user, since the validation will fetch the next tick, the half tick which their position is stashed on, and check the epoch and see that the half tick is a valid claim tick and therefore revert.
This prevents users from burning their liquidity when the position is in this state, a malicious actor can abuse this to prevent others from burning from their positions and keeping them trapped.
Recommendation
Initialize the
cache.priceClaimto be thepriceAtfor the half tick rather than the price at the half tick.Resolution
Poolshark Team: The recommendation was implemented in commit 0e40b90.
-
TK-8 High exactOut Does Not Function As Expected Logical Error Resolved
Description
The
amountLeftfor!exactInis increased in accordance with theswapFeein order to give the user exactly their specifiedamount. However theswapFeewill likely not apply to the entireamountOut, as theswapFeeapplies only to the portion ofamountOutthat was a direct result of the range pool liquidity.Therefore users specifying an
amountfor!exactInwill in most cases receive more than their definedamountout, which invalidates the definition of!exactIn.Recommendation
Compute and apply the fees to the
amountInfor the!exactIncase.Resolution
Poolshark Team: The recommendation was implemented in commit 90fb6e9.
-
LMP-3 Medium protocolFee0 Overwritten Logical Error Resolved
Description
When assigning fees with the fees function, the
LimitPoolManagerwill provide aprotocolFee0and aprotocolFee1, however theprotocolFee0will always be overwritten with theprotocolFee1.globalState.protocolFee = protocolFee0; globalState.protocolFee = protocolFee1;Therefore the
protocolFee1will always apply instead of theprotocolFee0which will result in unexpected fees being applied.Recommendation
Create
protocolFee0andprotocolFee1attributes on theILimitPoolStructs.GlobalStatestruct to store eachprotocolFee.Resolution
Poolshark Team: The recommendation was implemented in commit 80501d6.
-
GLOBAL-3 Medium Fee-On-Transfer Tokens Fee-on-transfer Acknowledged
Description
The
LimitPoolFactoryallows for permissionless creation of aLimitPoolwith anytokenInandtokenOut. Therefore atoken0ortoken1with fee-on-transfer or rebase mechanisms may be supplied.In the
transferInfunction there is logic to handle fee-on-transfer and rebase tokens. However in the mint call the returned value is not used in the mint process.Therefore even though there is logic built in to support fee on transfer tokens, it is not used.
Recommendation
Refactor the
mintCalland other relevant functions to rely on the returned value fromtransferInto account for fee on transfer tokens. Otherwise make it well documented that fee-on-transfer and rebase tokens are not compatible with the system.Resolution
Poolshark Team: Acknowledged.
-
POS-3 Medium Small Prices Round Out of Range When Multiplied Rounding Acknowledged
Description
When minting a position with a lower tick at or below tick
-665460, mints will begin to revert with thepriceOutOfBoundserror. This is because thegetLiquidityForAmountsfunction returns 0 when the product ofpriceLowerandpriceUpperis less thanQ96.When
liquidityMintedis zero it causes thepriceLimitto be 0 as well. Therefore failing the validation in thegetTickAtPricefunction as 0 is less than the price limit.if (price < constants.bounds.min || price >= constants.bounds.max) require (false, 'PriceOutOfBounds()');If the price of a pool were in this range user’s ability to mint positions would be limited as they would have to increase the range well beyond market price to mint successfully.
Recommendation
Consider implementing a solution for the
getLiquidityForAmountsfunction when the product of thepriceLowerandpriceUpperare less thanQ96. Otherwise ensure this behavior is clearly documented for traders and users deploying pools.Resolution
Poolshark Team: Acknowledged.
-
POS-4 Medium Resizes Without Swaps Are Too Severe Logical Error Resolved
Description
When positions are resized and the
priceLimitis not past the current pool price the resulting position is still resized to thepriceLimit, rather than the current market price.This is unexpected for the user and does not allow their position to immediately begin filling as price moves in their direction.
Recommendation
Add the following after fetching the cache.priceLimit in order to resize the user to the current market price in the event that a swap cannot occur.
if (ConstantProduct.withinBounds(cache.swapPool.price, cache.constants) && (params.zeroForOne ? cache.priceLimit > cache.swapPool.price : cache.priceLimit < cache.swapPool.price)) { cache.priceLimit = cache.swapPool.price; }Resolution
Poolshark Team: The recommendation was implemented in commit 501f609.
-
POS-5 Medium Misleading liquidityBurned emitted Events Resolved
Description
The Burn functionality allows users to burn a fully filled position with a nonzero
params.amount. In this case the regular burnparams.amount > 0logic is entered and theposition.liquidityis decremented by theparams.amount.Subsequently the position removal case is entered and the
params.amountis updated to be theposition.liquidityafter theposition.liquidityhas been reduced by the originalparams.amount.The
params.amountis then used to emit theBurnLimitevent as theliquidityBurnedparameter.Therefore a user can mislead other parties relying on the
BurnLimitevent by providing a nonzeroparams.amountwhen burning their fully filled position. In the worst case this event may emit aliquidityBurnedof 0, while the entire originalposition.liquiditywas actually burned.Recommendation
Allowing users to provide a nonzero
params.amountwhen they are burning a fully filled position introduces more avenues for exploitation and inconsistency. Do not allow users to pass a nonzeroparams.amountwhen they are burning their fully filled position, or auto update the amount to be 0 inClaims.validate.Resolution
Poolshark Team: The recommendation was implemented in commit 0e40b90.
-
POS-6 Medium Position Minted With 0 Liquidity Logical Error Resolved
Description
Proof of concept: PoC
It is possible to insert a position into the
positionsmapping with 0 liquidity. When minting a position, the initialcache.liquidityMintedinPositions.resizemay be greater than 0, bypassing theif(cache.liquidityMinted == 0) require (false, 'PositionLiquidityZero()')check.However, when the
cache.liquidityMintedis recalculated in the below snippet, it can become 0 after accounting for the swap.if (params.amount > 0 && params.lower < params.upper) cache.liquidityMinted = ConstantProduct.getLiquidityForAmounts( cache.priceLower, cache.priceUpper, params.zeroForOne ? cache.priceLower : cache.priceUpper, params.zeroForOne ? 0 : uint256(params.amount), params.zeroForOne ? uint256(params.amount) : 0 );This causes the user slight loss as any leftover
params.amountwill be unclaimable.Recommendation
Check if the
cache.liquidityMinted == 0on recalculation, and revert if so.Resolution
Poolshark Team: The recommendation was implemented in commit 5dc6b2b.
-
FMATH-1 Medium Inaccurate Fee On Event Events Resolved
Description
The
cache.feeAmountis increased by thelocals.feeAmountafter thelocals.feeAmountis decreased by thelocals.protocolFeesAccrued.locals.feeAmount -= locals.protocolFeesAccrued; // add to total fees paid for swap cache.feeAmount += locals.feeAmount.toUint128();As a result, the
cache.feeAmountemitted in theSwapevent does not truly represent how much the user paid in fees after swapping, since the protocol fees are not accounted for.Recommendation
Include the protocol fees the user paid as part of the
cache.feeAmount.Resolution
Poolshark Team: The recommendation was implemented in commit c312551.
-
TMAP-3 Medium Inclusive Skips Half Tick On Next Logical Error Resolved
Description
In the case that the
tickAtPriceis past the half tick, callingnext()withinclusive=Truedoes not return the the half tick when it is set. This could potentially lead to problems with liquidity not being activated which causes further negative consequences such as underflow, swap failure, among other things.Recommendation
Amend the inclusive logic such that a tick that is past the half tick but not yet at the full tick rounds back to the half tick by reducing the magnitude of the tick by half of the
tickSpacing.Resolution
Poolshark Team: The recommendation was implemented in commit 3356f37.
-
BCALL-2 Medium State Saved After Token Transfer Reentrancy Resolved
Description
In the
performfunction theCollect.burnLimitfunction which transfers tokens is executed before saving the state of the position.Poolshark allows the creating of a
LimitPoolwith anytokenInandtokenOut, some of these tokens will have callback capabilities. In the event that tokens with callbacks are used the state of the position should be saved before executing any token transfers, this way avoiding read-only reentrancy risks for systems interacting with Poolshark.Recommendation
Save the state of the position before transferring out tokens to the receiver.
Resolution
Poolshark Team: The recommendation was implemented in commit 77f9c26.
-
LMP-4 Medium Quote and Snapshot Read-only Reentrancy Risk Reentrancy Resolved
Description
The
quoteandsnapshot viewfunctions lack reentrancy protection and therefore allow a malicious user to execute a flashloan swap from theLimitPooland manipulate the output ofquote/snapshot to exploit systems interacting with theLimitPooland relying on theseviewfunctions.Recommendation
Add reentrancy protections to the
quoteandsnapshotfunctions such that these functions revert if the system has already been entered during the transaction.Resolution
Poolshark Team: The recommendation was implemented in commit 9458d42.
-
RPEI-1 Medium Missing ERC1155 Support Validation Validation Resolved
Description
The
mintFungiblefunction lacks acheckERC1155Supportmodifier to validate that the_accountaddress which will receive the minted tokens can handle the ERC1155 tokens.Recommendation
Add a
checkERC1155Support(_account)modifier to themintFungiblefunction.Resolution
Poolshark Team: The recommendation was implemented in commit 97d3d50.
-
PROU-2 Medium Lacking Slippage Controls Slippage Acknowledged
Description
The
PoolRoutercontract lacks any explicit slippage controls such as minimum output amount or maximum input amount. ThepriceLimitparameter for theLimitPool.swapfunction serves as user’s only form of protection from sandwich attacks, however user’s may desire additional explicit slippage controls such as minimum output or maximum input.Recommendation
Consider implementing minimum output and maximum input slippage controls in the
PoolRoutercontract.Resolution
Poolshark Team: Acknowledged.
-
LPF-1 Low Lack Of Token Validation Validation Resolved
Description
When creating a new
LimitPoolwith thecreateLimitPoolfunction there is no validation thattokenIn ≠tokenOutor that neithertokenInnortokenOutareaddress(0).Certainly pools where
token1 == token0are invalid. Additionally, pools wheretoken0isaddress(0)will attempt to use native ether via the functions inSafeTransfers.sol, however no functions are payable so clearly native ether is incompatible with the system.Recommendation
Add validation to check that
tokenIn ≠ tokenOutas well as thattoken0 ≠ address(0). Otherwise iftoken0should be allowed to beaddress(0)and native ether is indeed meant to be compatible with the system, make the appropriate functionspayableto allow native ether to be used in the system.Resolution
Poolshark Team: The suggested validations were implemented in the
createLimitPoolfunction. -
POS-7 Low Superfluous finalTick Assignment Superfluous Code Resolved
Description
The
finalTickis read from storage before being immediately assigned back to storage.{ // update max deltas ILimitPoolStructs.Tick memory finalTick = ticks[params.zeroForOne ? params.lower : params.upper]; ticks[params.zeroForOne ? params.lower : params.upper] = finalTick; }Recommendation
Remove the read and write for the
finalTickas there is no net effect.Resolution
Poolshark Team: The recommendation was implemented in commit 3682ae9.
-
POS-8 Low Superfluous Else Case Superfluous Code Resolved
Description
In the
removefunction, on line 225 anifcase performs validation on the position's liquidity and reverts if the case is entered.The following logic of the function is nested within an
elsecase, however thiselsecase is unnecessary as the contents of theifcase will always revert.Recommendation
Move the subsequent logic outside of the
elsecase to improve code readability and style.Resolution
Poolshark Team: The recommendation was implemented in commit 3682ae9.
-
GLOBAL-4 Low Unused Q96 Constant Superfluous Code Resolved
Description
In the
Positions.solandTicks.solfiles there is aQ96constant, but it is never used.Recommendation
Remove the
Q96constant from these files.Resolution
Poolshark Team: The recommendation was implemented in commit 3682ae9.
-
TK-9 Low Typo Typo Resolved
Description
The following comment contains a typo:
wouuld be smart to protect against the case of epochs crossing
Recommendation
would be smart to protect against the case of epochs crossing
Resolution
Poolshark Team: The recommendation was implemented in commit 3682ae9.
-
TK-10 Low Unnecessary Ternary Operator Superfluous Code Resolved
Description
In the
Ticks.insertfunction, ternary operators are used as conditionals where one case is alwaystrue. However these ternaries can be simplified in the following way:params.zeroForOne ?cache.priceLower > cache.pool.price :true→
!params.zeroForOne || cache.priceLower > cache.pool.priceparams.zeroForOne ?true :cache.priceUpper < cache.pool.price→
params.zeroForOne || cache.priceLower > cache.pool.price.Recommendation
Implement the above suggested simplifications.
Resolution
Poolshark Team: The recommendation was implemented in commit 91b0f3d.
-
LPF-2 Low Inefficient Token Assignment Optimization Resolved
Description
When assigning the
token0andtoken1for a new limit pool, two ternary operators are used to determine each token. However a single ternary operator can be used like so: (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenIn, tokenOut)Recommendation
Implement the above suggested optimization.
Resolution
Poolshark Team: The recommendation was implemented in commit 91b0f3d.
-
GLOBAL-5 Low Superfluous GlobalState Storage Variable Superfluous Code Resolved
Description
Throughout the codebase the
GlobalState globalStatestorage variable is used however theGlobalStatestruct only contains anunlockedvariable to facilitate reentrancy locks. However the reentrancy lock currently implemented by the system is a more error prone and less efficient version of OpenZeppelin’sReentrancyGuard.Recommendation
Remove the
globalStatevariable and use OpenZeppelin’sReentrancyGuard.Resolution
Poolshark Team: The
GlobalStatestorage variable is now used for more than a reentrancy lock. -
POS-9 Low Inconsistent mintPercent Decimals Consistency Acknowledged
Description
In the
Positions.resizefunction, theparams.mintPercentis treated as a percentage with1e28decimals, however this is inconsistent with the decimals of1e38used for liquidity percent conversions in the_convertfunction.Recommendation
Standardize on either
1e28or1e38for percentage decimals for consistency.Resolution
Poolshark Team: Acknowledged.
-
LMP-5 Low Lacking Zero Address Checks Validation Acknowledged
Description
There is no check for
params.to == address(0)when calling themintorswapfunctions inLimitPool.Recommendation
Implement the following check in the
mintandswapfunctions:if (params.to == address(0)) revert CollectToZeroAddress();Resolution
Poolshark Team: Acknowledged.
-
LMP-6 Low Typo Typo Acknowledged
Description
The
canonicalOnlymodifier is misspelled ascanoncialOnly.Recommendation
Replace all instances of
canoncialOnlywithcanonicalOnly.Resolution
Poolshark Team: Acknowledged.
-
EMAP-1 Low Redundant _tick Function Superfluous Code Acknowledged
Description
The
_tickfunction is implemented in both theEpochMapand theTickMapfiles. Only the_tickimplementation in theTickMapis used, therefore the implementation in theEpochMapcan be removed.Recommendation
Remove the
_tickfunction from theTickMap.Resolution
Poolshark Team: Acknowledged.
-
RLIB-1 Low Superfluous RebaseLibrary Superfluous Code Acknowledged
Description
Throughout the codebase the
RebaseLibrary.solfile is not used.Recommendation
Remove the unnecessary library.
Resolution
Poolshark Team: Acknowledged.
-
CLAIMS-6 Low Superfluous claimTick Assignments Superfluous Code Acknowledged
Description
The
cache.claimTickis assigned to in several cases throughout theClaims.validatefunction, however thecache.claimTickis never referenced after this assignment.Recommendation
Remove the assignments in the
Claims.validatefunction.Resolution
Poolshark Team: Acknowledged.
-
TK-11 Low Outdated Comments Documentation Acknowledged
Description
The comment
0 -> 1 positions price moves up so nextFullTick is lesseris not accurate on line 535 as the relevant tick is thepreviousFullTickrather than thenextFullTick.The comment
0 -> 1 positions price moves up so nextFullTick is lesseris not accurate on line 551 as this is the!zeroForOnecase.Recommendation
Update the comments in the
insertSinglefunction.Resolution
Poolshark Team: Acknowledged.
-
CLAIMS-7 Low Superfluous claimTickEpoch Assignments Superfluous Code Acknowledged
Description
The
claimTickEpochis assigned a value inside of the innerifcases on lines 43 and 61 but then re-assigned to the same exact value right after.Recommendation
Remove the assignments to
claimTickEpochon these lines as they have no effect.Resolution
Poolshark Team: Acknowledged.
-
SFST-1 Low Unused Checks Library Superfluous Code Acknowledged
Description
The
Checkslibrary includessaveandbalancefunctions which may be useful throughout the codebase, however this library is never used.Recommendation
Either implement usage of the
Checkslibrary or remove it from the codebase.Resolution
Poolshark Team: Acknowledged.
-
POS-10 Low Unreachable Code Superfluous Code Acknowledged
Description
The
ifstatement beginning on line 391 is unreachable as it requires theparams.claim ≠ loweras well asparams.claim == lowerwhich is unsatisfiable.Recommendation
Remove this
ifstatement as it is unreachable and unnecessary.Resolution
Poolshark Team: Acknowledged.
-
CLAIMS-8 Low Superfluous Early Return Logic Superfluous Code Acknowledged
Description
The early return logic in
Claims.validateis unreachable. The conditions that satisfy the early return case inClaims.validateconstitute callingPositions.removeinstead.To enter update (for
zeroForOne):cache.position.claimPriceLast != 0 || params.claim != params.lower || epochLower >positionEpochTo enter the early return case inside of update (for
zeroForOne):claimPriceLast == 0 && params.claim == params.lower && epochLower <= positionEpochNever will these two conditions both be satisfiable, therefore this early return case can never be reached.
Recommendation
Remove all of the early return logic as it is unreachable.
Resolution
Poolshark Team: Acknowledged.
-
POS-11 Low Superfluous Position Write Superfluous Code Acknowledged
Description
There is a positions mapping write in the
Positions.removefunction, however it will always write to the same lower and upper tick boundaries as the subsequent write in theBurnCall.performfunction.Recommendation
Remove the extraneous positions write in the
Positions.removefunction, additionally add a check in thePositions.removefunction that the claim is always exactly theparams.lowerforzeroForOneandparams.upperfor!zeroForOne,to be explicitly safe.Resolution
Poolshark Team: Acknowledged.
-
LMPM-1 Low Enabled Pool Configurations Cannot Be Disabled Configuration Acknowledged
Description
Once a
tickSpacingorimplementationis configured in theLimitPoolManagercontract they cannot be disabled.If an issue is discovered with any particular
tickSpacingorimplementationthen it cannot be removed and pools will still be allowed to be created with that misguided configuration.Recommendation
Implement functions to disable a particular
tickSpacingorimplementationin theLimitPoolManager.Resolution
Poolshark Team: Acknowledged.
-
POS-12 Low Redundant Validation Superfluous Code Acknowledged
Description
In the
removefunction, it is validated that theparams.amountis less than or equal to1e38. However this validation is already performed in the_convertfunction.Recommendation
Remove the first instance of the validation and perform the conversion at the beginning of the
removefunction.Resolution
Poolshark Team: Acknowledged.
-
GLOBAL-6 Low Superfluous refundTo Variable Superfluous Code Acknowledged
Description
There is a
refundToparameter on theMintParamsstruct, however it is never initialized or referenced.Recommendation
Either implement logic for the
refundToaddress or remove it from theMintParamsstruct.Resolution
Poolshark Team: Acknowledged.
-
GLOBAL-7 Low Floating Pragma Version Floating Pragma Resolved
Description
The smart contracts in the project use floating version of Solidity (^0.8.13). There is a list of known bugs in Solidity versions, many of them can impact smart contract functionality in unexpected way:
https://github.com/ethereum/solidity/blob/develop/docs/bugs.json.
Furthermore, if compiled with
0.8.20there may be unexpected reverts when deployed as some chains still do not supportPUSH0.Recommendation
Consider utilizing a static version.
Resolution
Poolshark Team: The recommendation was implemented in 0dd6a82.
-
GLOBAL-8 Low For Loop Optimizations Optimization Resolved
Description
Throughout the codebase several
forloops are used without caching the length of the array they iterate over or performing anunchecked { ++i }for optimization.Recommendation
Consider caching the length of the array to be iterated over and incrementing the index with
unchecked { ++i }.Resolution
Poolshark Team: The recommendation was implemented in 91ab44a.
-
POS-13 Low Superfluous Removal Logic Superfluous Code Acknowledged
Description
The
EpochMap.get(params.lower, tickMap, constants) > cache.position.epochLastandEpochMap.get(params.upper, tickMap, constants) > cache.position.epochLastconditions on lines 234 and 249 are unsatisfiable as these are conditions to enterPositions.updaterather thanPositions.remove.In fact if these conditions were satisfiable a critical bug would arise where the
position.liquidityis decremented from thepool.liquidityas well as the position’s upper and lower ticks.Recommendation
Remove the logic from lines 232 to 261.
Resolution
Poolshark Team: Acknowledged.
-
GLOBAL-9 Low Unsafe Casting Casting Acknowledged
Description
Throughout the codebase, casting operations are performed. Downcasting does not revert on overflow, therefore it would be prudent to use OpenZeppelin’s SafeCast to revert in these cases.
Recommendation
Consider using OpenZeppelin’s SafeCast to protect against undetected overflow.
Resolution
Poolshark Team: Acknowledged.
-
GLOBAL-10 Low Dust Positions Negatively Impact The Protocol Manipulation Acknowledged
Description
Malicious actors are allowed to create many small range positions with only a few wei of liquidity in order to initialize many ticks and make swaps more gas consumptive. Additionally, position’s with minute liquidity and narrow ranges serve little purpose but to potentially manipulate the
LimitPoolin unforseen ways.Recommendation
Consider implementing a minimum liquidity for position’s to avoid potential manipulation or griefing, potentially based on the width of the position.
Resolution
Poolshark Team: Acknowledged.
-
CLAIMS-9 Low Malicious Burn DoS DoS Acknowledged
Description
As a side effect of the
positions[params.owner][params.claim][params.upper].liquidity > 0check in the claims validation, a malicious actor can front-run a user’s burn and mint a dust position for the user such that this claim validation check fails. The user would then have to burn the dust position before being able to claim their existing position.Recommendation
Consider removing the ability for users to mint for other users or ensure a minimum liquidity amount is implemented such that any griefing attacks pose a non-trivial loss to the actors.
Resolution
Poolshark Team: Acknowledged.
-
RNP-1 Low Liquidity Overflow Check Is Inconsistent Validation Acknowledged
Description
The
RangePositions.validatefunction only performs validation that theliquidityMintedis less than the maxint128value, however fails to validate that theliquidityGlobal + liquidityMintedis less than the maxuint128value.This validation is later performed in the
RangeTicks.insertfunction, however the disparity in validation poses risk for any future code changes.Recommendation
Include the
liquidityGlobalvalidation in theRangePositions.validatefunction.Resolution
Poolshark Team: Acknowledged.
-
TK-12 Low Superfluous cache.liquidity Adjustments Superfluous Code Acknowledged
Description
In the
_crossfunction thecache.liquidityamount is adjusted for theLIMIT_TICKandLIMIT_POOLcross statuses. However thecache.liquidityis immediately overwritten in the_iteratefunction, therefore thecache.liquiditywrites in_crosshave no effect.Recommendation
Remove the unnecessary
cache.liquidityadjustments in the_crossfunction.Resolution
Poolshark Team: Acknowledged.
-
POS-14 Low Position Unnecessarily Deleted Optimization Acknowledged
Description
The claim tick is rounded back to the earlier full tick after performing the position removal check on line 417. However in the old position would not need to be cleared when the
params.claimis a half tick ahead of the position's lower tick.Recommendation
Perform
params.claim = TickMap.roundBack(params.claim, constants, params.zeroForOne,cache.priceClaim)before checking if clearing the original position is necessary on line 417.Resolution
Poolshark Team: Acknowledged.
-
CLAIMS-10 Low Futile Burns Are Allowed Validation Acknowledged
Description
The validation logic aims to prevent any burns which would result in no net change to the existing position with a
NoPositionUpdateserror.However a burn with an amount of 0 and a
claimTickwhich is justtickSpacing/2ahead of the position's start tick will result in no net change to the existing position yet get past the validation logic.Recommendation
Do not allow burns with an amount of 0 for claims where the
claimTickrounds back to the start tick of the position.Resolution
Poolshark Team: Acknowledged.
-
TK-13 Low Unnecessary uint256 Casting Superfluous Code Acknowledged
Description
Throughout the
Ticks._quoteSinglefunction thecache.pricevariable is cast asuint256. However these variables are alreadyuint256variables and so therefore do not need to be cast as such.Recommendation
Do not cast the
cache.pricevariable as auint256.Resolution
Poolshark Team: Acknowledged.
-
GLOBAL-11 Low Redundant Boolean Logic In _empty Optimization Acknowledged
Description
The
_emptyfunctions inRangeTicksandLimitTicksuse anifcondition to returntrueorfalse.However the
_emptyfunction can simply return the result of the condition rather than using anif.Recommendation
Return the result of the
liquidityAbsolute != 0check directly.Resolution
Poolshark Team: Acknowledged.
-
MRC-1 Low Inefficient Validation Validation Acknowledged
Description
The
Positions.validatecall can be performed before thePositions.updateas any update would be invalid if the provided lower/upper ticks are invalid.Recommendation
Perform
Positions.validatebeforePositions.update.Resolution
Poolshark Team: Acknowledged.
-
RTK-1 Low Superfluous Else Case Superfluous Code Acknowledged
Description
The contents of the
elsecase on lines 95 and 130 are exactly that of the aboveifcase where the tick already existed.Recommendation
Rather than introducing another separate case with the same logic, adjust the original
ifcases to include thelower > tickAtPriceandupper > tickAtPricecases.Resolution
Poolshark Team: Acknowledged.
-
GLOBAL-12 Low Clones Can Receive ETH But Not Withdraw It Trapped Ether Acknowledged
Description
The clones deployed with the
cloneDeterministicfunction implement areceivefunction however neither theLimitPoolnor theRangePoolIERC1155implementations have methods to withdraw Ether that may be errantly sent to the clone.Recommendation
Implement safety Ether withdrawal functions in case Ether is errantly transferred to the cloned contracts.
Resolution
Poolshark Team: Acknowledged.
-
BRC-1 Low Superfluous Addition And Subtraction Optimization Acknowledged
Description
In the
BurnRangeCall.performfunction theposition.amount0andposition.amount1are decremented by thecache.amount0andcache.amount1after theRangePositions.removecall. The goal of this is simply to remove the burned fees amount from the position.However the subtraction takes place after the
RangePositions.removecall, where thecache.amount0andcache.amount1are incremented by the position liquidity removed by the burn.For this reason the
position.amount0andposition.amount1are also increased by the removed liquidity amounts in theRangePositions.removefunction.However this unnecessary addition can be avoided by simply subtracting the
cache.amount0andcache.amount1from the position amounts directly after theRangePositions.updatecall where thecache.amount0andcache.amount1represent exactly the burned fee amounts.Recommendation
Move the
position.amount0andposition.amount1subtractions directly after theRangePositions.updatecall.Resolution
Poolshark Team: Acknowledged.
No findings match.
Invariants 22
The review's fuzzing suite asserted 22 invariants. 22 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
GLOBAL-1 | The upper position boundary is above the lower boundary | Held |
GLOBAL-2 | The lower and upper ticks are on full ticks | Held |
GLOBAL-3 | The price of pool0 is always greater than or equal to the price of pool1 | Held |
GLOBAL-4 | Pool liquidity never underflows | Held |
GLOBAL-5 | liquidityGlobal never underflows | Held |
GLOBAL-6 | liquidityAbsolute never underflows | Held |
GLOBAL-7 | Pool liquidity never overflows | Held |
GLOBAL-8 | Transfer amount never exceeds pool balance | Held |
GLOBAL-9 | Full ticks have a priceAt of 0 | Held |
MINT-1 | Global liquidity increases when minting a position | Held |
MINT-2 | Pool liquidity is non-zero on undercut | Held |
MINT-3 | Minting a position then burning the position causes no change in global liquidity | Held |
MINT-4 | liquidityDelta is always less than or equal to liquidityAbsolute | Held |
MINT-5 | liquidityAbsolute increases when not undercutting price | Held |
MINT-6 | Pool liquidity is positive on unlock | Held |
BURN-1 | Burning a position decreases global liquidity | Held |
TMAP-1 | Tick exists if set twice | Held |
TMAP-2 | Tick does not exist if set the unset | Held |
TMAP-3 | Tick exists when set | Held |
TMAP-4 | Next tick includes half tick when inclusive | Held |
TMAP-5 | Tick does not exist when unset | Held |
TMAP-6 | Previous tick includes half/full tick when inclusive | Held |
More from Poolshark
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
