LP pool cap may be exceeded on drawing settlement
| Title | LP pool cap may be exceeded on drawing settlement |
|---|---|
| Reward | $9542, 2 dups |
| Contest | Megapot - 3 November 2025 on Code4rena |
| Author | montecristo |
| Context | Cap exceeded |
Jackpot enforces pool cap as the following:
function _calculateLpPoolCap(uint256 _normalBallMax) internal view returns (uint256) {
// We use MAX_BIT_VECTOR_SIZE because that's the max number that can be packed in a uint256 bit vector
uint256 maxAllowableTickets = Combinations.choose(_normalBallMax, NORMAL_BALL_COUNT) * (MAX_BIT_VECTOR_SIZE - _normalBallMax);
uint256 maxPrizePool = maxAllowableTickets * ticketPrice * (PRECISE_UNIT - lpEdgeTarget) / PRECISE_UNIT;
// We need to make sure that the lpPoolCap is not greater than the governance pool cap
return Math.min(maxPrizePool * PRECISE_UNIT / (PRECISE_UNIT - reserveRatio), governancePoolCap);
}
This is to ensure the following:
- bonusBallMax + normalBallMax <= MAX_BIT_VECTOR_SIZE
- Pool cap does not exceed governance pool cap
Otherwise TicketComboTracker cannot store a purchased ticket at the maximum bonus ball, because the following reverts with an overflow:
ticketNumbers = set |= 1 << (_bonusball + _tracker.normalMax);
However, LP pool cap can be exceeded on drawing settlement, because new LP value calculation does not enforce the same pool cap logic:
function processDrawingSettlement(
uint256 _drawingId,
uint256 _lpEarnings,
uint256 _userWinnings,
uint256 _protocolFeeAmount
) external onlyJackpot() returns (uint256 newLPValue, uint256 newAccumulator) {
LPDrawingState storage currentLP = lpDrawingState[_drawingId];
uint256 postDrawLpValue = currentLP.lpPoolTotal + _lpEarnings - _userWinnings - _protocolFeeAmount;
// ... (other logic between lines 379-390 was omitted in your snippet)
newLPValue = postDrawLpValue + currentLP.pendingDeposits - withdrawalsInUSDC;
}
Since the LP value can grow by up to lpEdgeTarget = 30% on every draw with no jackpot winner, the governance cap or the calculated limit can be exceeded whenever the previous total pool sat just below it.
Impact
Important invariants can be broken on settlement drawing:
- Pool Cap Compliance: Total pool never exceeds governance limits
- Pool cap enforcement:
lpPoolTotal + pendingDeposits <= governancePoolCap - Is the bit-packing logic sound? Are there boundary errors between the lower bits, where the normal balls sit, and the higher bits, where the bonus ball must stay below
255 - normalBallMax?
In particular, once the new pool cap exceeds the calculated safe limit, bonusBallMax can exceed 255 - normalBallMax.
That denies service to normalBallMax betting, and ultimately makes the betting unfair.
Alpha: read every stated invariant and confirm none can be broken. Here, a per-variable analysis is enough: check each update and whether it is capped.
Conclusion
This finding would earn you $9542, and needed nothing more than checking whether the stated invariant could break.