Platform: Code4rena
Start Date: 01/12/2022
Pot Size: $60,500 USDC
Total HM: 8
Participants: 27
Period: 11 days
Judge: kirk-baird
Total Solo HM: 6
Id: 187
League: ETH
Rank: 1/27
Findings: 4
Award: $17,868.05
🌟 Selected for report: 3
🚀 Solo Findings: 3
🌟 Selected for report: hansfriese
5936.0731 USDC - $5,936.07
Pool._amountToBin()
returns a larger value than it should when protocolFeeRatio = 100%
.
As a result, bin balances might be calculated wrongly.
delta.deltaInBinInternal
is used to update the bin balances like this.
if (tokenAIn) { binBalanceA += delta.deltaInBinInternal.toUint128(); binBalanceB = Math.clip128(binBalanceB, delta.deltaOutErc.toUint128()); } else { binBalanceB += delta.deltaInBinInternal.toUint128(); binBalanceA = Math.clip128(binBalanceA, delta.deltaOutErc.toUint128()); }
As we can see here, _amountToBin()
is used to calculate delta.deltaInBinInternal
from deltaInErc
and feeBasis
.
uint256 feeBasis = Math.mulDiv(binAmountIn, fee, PRBMathUD60x18.SCALE - fee, true); delta.deltaInErc = binAmountIn + feeBasis; delta.deltaInBinInternal = _amountToBin(delta.deltaInErc, feeBasis); delta.excess = swapped ? Math.clip(amountOut, delta.deltaOutErc) : 0;
With the above code, the protocol fee should be a portion of feeBasis
and it is the same as feeBasis
when state.protocolFeeRatio = ONE_3_DECIMAL_SCALE
.
But when we check _amountToBin()
, the actual protocol fee will be feeBasis + 1
and delta.deltaInBinInternal
will be less than its possible smallest value(= binAmountIn
).
function _amountToBin(uint256 deltaInErc, uint256 feeBasis) internal view returns (uint256 amount) { amount = state.protocolFeeRatio != 0 ? Math.clip(deltaInErc, feeBasis.mul(uint256(state.protocolFeeRatio) * PROTOCOL_FEE_SCALE) + 1) : deltaInErc; }
Manual Review
We should modify _amountToBin()
like below.
function _amountToBin(uint256 deltaInErc, uint256 feeBasis) internal view returns (uint256 amount) { if (state.protocolFeeRatio == ONE_3_DECIMAL_SCALE) return deltaInErc - feeBasis; amount = state.protocolFeeRatio != 0 ? Math.clip(deltaInErc, feeBasis.mul(uint256(state.protocolFeeRatio) * PROTOCOL_FEE_SCALE) + 1) : deltaInErc; }
#0 - c4-sponsor
2022-12-16T03:26:26Z
gte620v marked the issue as sponsor acknowledged
#1 - c4-judge
2023-01-09T09:32:32Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: hansfriese
5936.0731 USDC - $5,936.07
Time-warped-price is updated incorrectly and this affects moving bins.
The protocol updates twa
on every swap and uses that to decide how to move bins.
But in the function swap()
, the delta's endSqrtPrice
can not contribute negatively to the activeTick
because it is wrapped with a clip()
function.
// Pool.sol 286: if (amountOut != 0) { 287: if (tokenAIn) { 288: binBalanceA += delta.deltaInBinInternal.toUint128(); 289: binBalanceB = Math.clip128(binBalanceB, delta.deltaOutErc.toUint128()); 290: } else { 291: binBalanceB += delta.deltaInBinInternal.toUint128(); 292: binBalanceA = Math.clip128(binBalanceA, delta.deltaOutErc.toUint128()); 293: } 294: twa.updateValue(currentState.activeTick * PRBMathSD59x18.SCALE + int256(Math.clip(delta.endSqrtPrice, delta.sqrtLowerTickPrice).div(delta.sqrtUpperTickPrice - delta.sqrtLowerTickPrice)));//@audit second part of the sum is always positive, should contribute both ways 295: } 296:
I believe delta.endSqrtPrice
should contribute both ways to the activeTick
and it is quite possible for the endSqrtPrice
to be out of the range (delta.sqrtLowerTickPrice, delta.sqrtUpperTickPrice)
. (In another report, I mentioned an issue of accuracy loss in the calculation of endSqrtPrice
).
Manual Review
I recommend changing the relevant line as below without using clip()
so that the endSqrtPrice
can contribute to the twa
reasonably.
294: twa.updateValue(currentState.activeTick * PRBMathSD59x18.SCALE + int256((delta.endSqrtPrice, delta.sqrtLowerTickPrice).div(delta.sqrtUpperTickPrice - delta.sqrtLowerTickPrice)));
#0 - gte620v
2022-12-16T04:48:57Z
The suggested mitigation is not correct, but this did alert us a related issue in calculating the fractional part of the log TWAP when swap to sqrtPriceLimit
is used for tokenAIn=false
and when there is a gap between bins. In that case, the fractional part of the twap is not accurate. We will implement a fix.
#1 - c4-sponsor
2022-12-16T04:49:03Z
gte620v marked the issue as sponsor confirmed
#2 - c4-judge
2023-01-12T22:00:32Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: hansfriese
5936.0731 USDC - $5,936.07
Judge has assessed an item in Issue #100 as M risk. The relevant finding follows:
Lines of code https://github.com/code-423n4/2022-12-Stealth-Project/blob/fc8589d7d8c1d8488fd97ccc46e1ff11c8426ac2/maverick-v1/contracts/libraries/Bin.sol#L35 https://github.com/code-423n4/2022-12-Stealth-Project/blob/fc8589d7d8c1d8488fd97ccc46e1ff11c8426ac2/maverick-v1/contracts/libraries/Bin.sol#L38
Vulnerability details Impact The wrong amount of LP tokens will be minted and the wrong amount of A/B tokens will be deposited.
Proof of Concept According to the PDF document provided, the number of LP tokens newSupply is calculated using the Table 1 as belows. Imgur
This is implemented in the function lpTokensFromDeltaReserve() of Bin.sol as belows.
function lpTokensFromDeltaReserve( Bin.Instance storage self, uint256 _deltaA, uint256 _deltaB, int32 _activeLowerTick, uint256 _existingReserveA, uint256 _existingReserveB ) internal view returns (uint256 deltaAOptimal, uint256 deltaBOptimal, uint256 proRataLiquidity) { deltaAOptimal = _deltaA; deltaBOptimal = _deltaB; bool noA = self.state.reserveA == 0; bool noB = self.state.reserveB == 0; if (self.state.lowerTick < _activeLowerTick || (!noA && noB)) {//@audit case p<p_l, the inequality should be opposite side deltaBOptimal = 0; proRataLiquidity = noA || self.state.totalSupply == 0 ? deltaAOptimal : Math.mulDiv(deltaAOptimal, self.state.totalSupply, self.state.reserveA, false); } else if (self.state.lowerTick > _activeLowerTick || (noA && !noB)) {//@audit case p>p_u, the inequality should be opposite side deltaAOptimal = 0; proRataLiquidity = noB || self.state.totalSupply == 0 ? deltaBOptimal : Math.mulDiv(deltaBOptimal, self.state.totalSupply, self.state.reserveB, false); } else { if (_existingReserveA > 0) { deltaBOptimal = Math.mulDiv(_existingReserveB, _deltaA, _existingReserveA, false); } if (deltaBOptimal > _deltaB && _existingReserveB > 0) { deltaAOptimal = Math.mulDiv(_existingReserveA, _deltaB, _existingReserveB, false); deltaBOptimal = _deltaB; } proRataLiquidity = (noA && noB) || self.state.totalSupply == 0 ? Math.max(deltaAOptimal, deltaBOptimal) : Math.min(Math.mulDiv(deltaAOptimal, self.state.totalSupply, self.state.reserveA, false), Math.mulDiv(deltaBOptimal, self.state.totalSupply, self.state.reserveB, false)); } }
The implementation uses wrong inequalities for the edge cases $p\lt p_l$ and $p\gt p_u$.
Tools Used Manual Review
Recommended Mitigation Steps Reverse the inequalities at Bin.sol#L35 and Bin.sol#L38.
#0 - c4-judge
2023-01-22T22:19:49Z
kirk-baird marked the issue as satisfactory
#1 - kirk-baird
2023-01-22T22:23:41Z
This is exactly issue #100 since it was downgraded to QA a new issue needed to be created to upgrade it to Medium.
There is a bug in the white paper which would have been a critical issue. However, this bug was not replicated in the code base. After discussion on the c4 community we are currently considering this situation to be a medium severity issue.
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xDecorativePineapple, Chom, Deivitto, IllIllI, Jeiwan, Josiah, Mukund, RaymondFam, Rolezn, ajtra, cccz, chrisdior4, csanuragjain, hansfriese, ladboy233, minhquanym, pedr02b2, peritoflores, rvierdiiev, sakshamguruji, sces60107
59.8382 USDC - $59.84
Pool.checkPositionAccess()
doesn't check the approvedForAll
users.function checkPositionAccess(uint256 tokenId) internal view { require(msg.sender == position.ownerOf(tokenId) || msg.sender == position.getApproved(tokenId), "P"); }
Currently, it checks only the owner or approved user for the tokenId
for the access permission.
But according to the openzeppelin implement, it should allow isApprovedForAll
users as well because such users can be the token owner without any requirements by transferring the token.
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { address owner = ERC721.ownerOf(tokenId); return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); }
Math.mulDiv()
returns a wrong value when ceil = true
function mulDiv(uint256 x, uint256 y, uint256 k, bool ceil) internal pure returns (uint256) { if (y == k) return x; return ceil ? PRBMath.mulDiv(x, y, k) + 1 : PRBMath.mulDiv(x, y, k); }
Currently, ceilVal = floorVal + 1
all the time but they should be same when PRBMath.mulDiv(x, y, k)
is an exact uint value without roundings.
Pool.setProtocolFeeRatio()
should have a upper limit.When we set a protocolFeeRatio
, it can be ONE_3_DECIMAL_SCALE
which means 100% of the pool fee.
Then all amounts of feeBasis
will be a protocol fee here, no benefit for liquidity providers.
When users or admin set the recipient
, there is no validation of address(0) so the funds might be lost.
"amount of B token in bin" => "amount of A token in bin"
#0 - kirk-baird
2022-12-15T10:55:03Z
The first issue is a duplicate of #9 however that will likely be considered QA.
#1 - c4-judge
2022-12-15T10:56:02Z
kirk-baird marked the issue as grade-a
#2 - c4-judge
2023-01-18T22:19:31Z
kirk-baird marked the issue as grade-b