The protocol states that a winner has 1 year to claim prizes, after that prizes go back to the pot if not claimed. The protocol implements this time frame as 52 draws as they expect to configure one draw per week (DRAWS_PER_YEAR
constant defined in LotteryMath
).
From the docs:
Winning tickets have up to 1 year to claim their prize. If a prize is not claimed before this period, the unclaimed prize money will go back to the Prize Pot.
However, technically this is not correctly implemented, as the protocol will set the limit to 52 periods but will offset this limit with the cooldown period. The claimable
function uses the ticketRegistrationDeadline
function to calculate the limit:
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L159-L168
function claimable(uint256 ticketId) external view override returns (uint256 claimableAmount, uint8 winTier) {
TicketInfo memory ticketInfo = ticketsInfo[ticketId];
if (!ticketInfo.claimed) {
uint120 _winningTicket = winningTicket[ticketInfo.drawId];
winTier = TicketUtils.ticketWinTier(ticketInfo.combination, _winningTicket, selectionSize, selectionMax);
if (block.timestamp <= ticketRegistrationDeadline(ticketInfo.drawId + LotteryMath.DRAWS_PER_YEAR)) {
claimableAmount = winAmount[ticketInfo.drawId][winTier];
}
}
}
However, the ticketRegistrationDeadline
function offsets this limit the cooldown period:
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotterySetup.sol#L156-L158
function ticketRegistrationDeadline(uint128 drawId) public view override returns (uint256 time) {
time = drawScheduledAt(drawId) - drawCoolDownPeriod;
}
This means that a user holding an unclaimed winner ticket won't be able to claim their prize at the very end of the claimable period.
----------------------------------------------------------------------------------------------
| | |
Ticket draw | ... +50 draws ... | +51 draw | Cool down |
| | |
-----------------------------------------------------------------------------------------------
^ ^ ^ ^
ticket draw ends start of last user can't claim should end here
claimable period starts claimable period from here
Ticket winners won't be able to claim their prizes at the very end of the claimable period and will lose their funds while they still technically should have enough time until the last claimable draw finishes.
The following test reproduces the issue. A user wins the jackpot and is entitled to the jackpot prize. We then advance time until the last claimable period minus the cooldown period. The user now has nothing to claim, the call to claimWinningTickets
will revert.
contract AuditTest is LotteryTestBase {
function test_Lottery_claimRewards_RewardLostAtEndOfClaimablePeriod() public {
address user = makeAddr("user");
uint120 ticket = uint120(0x0321);
uint256 randomNumber = 0x01020304;
vm.startPrank(user);
uint256[] memory ticketIds = buySameTickets(lottery.currentDraw(), ticket, address(0), 1);
vm.stopPrank();
finalizeDraw(randomNumber);
uint256 claimableAmount;
uint256 jackpotPrize = lottery.currentRewardSize(SELECTION_SIZE);
// User won jackpot and can claim reward
(claimableAmount,) = lottery.claimable(ticketIds[0]);
assertEq(claimableAmount, jackpotPrize);
// Advance 52 period minus the cooldown period
vm.warp(block.timestamp + 52 * PERIOD - COOL_DOWN_PERIOD);
// User claimable is now 0! Should still be able to claim until the last period finishes
(claimableAmount,) = lottery.claimable(ticketIds[0]);
assertEq(claimableAmount, 0);
// Claiming will revert with NothingToClaim error
vm.prank(user);
vm.expectRevert(abi.encodeWithSelector(NothingToClaim.selector, ticketIds[0]));
lottery.claimWinningTickets(ticketIds);
}
}
The claimable
function should use drawScheduledAt
instead of ticketRegistrationDeadline
to correctly implement the claimable limit:
function claimable(uint256 ticketId) external view override returns (uint256 claimableAmount, uint8 winTier) {
TicketInfo memory ticketInfo = ticketsInfo[ticketId];
if (!ticketInfo.claimed) {
uint120 _winningTicket = winningTicket[ticketInfo.drawId];
winTier = TicketUtils.ticketWinTier(ticketInfo.combination, _winningTicket, selectionSize, selectionMax);
if (block.timestamp <= drawScheduledAt(ticketInfo.drawId + LotteryMath.DRAWS_PER_YEAR)) {
claimableAmount = winAmount[ticketInfo.drawId][winTier];
}
}
}