diff --git a/src/PrizePool.sol b/src/PrizePool.sol index 4057f97..cf9c90f 100644 --- a/src/PrizePool.sol +++ b/src/PrizePool.sol @@ -93,6 +93,9 @@ error PrizeIsZero(); /// @notice Emitted when someone tries to claim a prize, but sets the fee recipient address to the zero address. error FeeRecipientZeroAddress(); +/// @notice Emitted when a claim is attempted after the claiming period has expired. +error ClaimPeriodExpired(); + /** * @notice Constructor Parameters * @param prizeToken The token to use for prizes @@ -165,10 +168,10 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { uint48 drawStartedAt ); - /// @notice Emitted when any amount of the reserve is withdrawn. - /// @param to The address the assets are transferred to - /// @param amount The amount of assets transferred - event WithdrawReserve(address indexed to, uint256 amount); + /// @notice Emitted when any amount of the reserve is rewarded to a recipient. + /// @param to The recipient of the reward + /// @param amount The amount of assets rewarded + event AllocateRewardFromReserve(address indexed to, uint256 amount); /// @notice Emitted when the reserve is manually increased. /// @param user The user who increased the reserve @@ -182,10 +185,11 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { event ContributePrizeTokens(address indexed vault, uint24 indexed drawId, uint256 amount); /// @notice Emitted when an address withdraws their prize claim rewards. + /// @param account The account that is withdrawing rewards /// @param to The address the rewards are sent to /// @param amount The amount withdrawn /// @param available The total amount that was available to withdraw before the transfer - event WithdrawClaimRewards(address indexed to, uint256 amount, uint256 available); + event WithdrawRewards(address indexed account, address indexed to, uint256 amount, uint256 available); /// @notice Emitted when an address receives new prize claim rewards. /// @param to The address the rewards are given to @@ -205,8 +209,8 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { mapping(address vault => mapping(address account => mapping(uint24 drawId => mapping(uint8 tier => mapping(uint32 prizeIndex => bool claimed))))) internal _claimedPrizes; - /// @notice Tracks the total fees accrued to each claimer. - mapping(address claimer => uint256 rewards) internal _claimerRewards; + /// @notice Tracks the total rewards accrued for a claimer or draw completer. + mapping(address recipient => uint256 rewards) internal _rewards; /// @notice The degree of POOL contribution smoothing. 0 = no smoothing, ~1 = max smoothing. /// @dev Smoothing spreads out vault contribution over multiple draws; the higher the smoothing the more draws. @@ -335,10 +339,10 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { return _deltaBalance; } - /// @notice Allows the Manager to withdraw tokens from the reserve. - /// @param _to The address to send the tokens to - /// @param _amount The amount of tokens to withdraw - function withdrawReserve(address _to, uint96 _amount) external onlyDrawManager { + /// @notice Allows the Manager to allocate a reward from the reserve to a recipient. + /// @param _to The address to allocate the rewards to + /// @param _amount The amount of tokens for the reward + function allocateRewardFromReserve(address _to, uint96 _amount) external onlyDrawManager { if (_amount > _reserve) { revert InsufficientReserve(_amount, _reserve); } @@ -347,8 +351,8 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { _reserve -= _amount; } - _transfer(_to, _amount); - emit WithdrawReserve(_to, _amount); + _rewards[_to] += _amount; + emit AllocateRewardFromReserve(_to, _amount); } /// @notice Allows the Manager to close the current open draw and open the next one. @@ -425,6 +429,12 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { uint96 _fee, address _feeRecipient ) external returns (uint256) { + /// @dev Claims cannot occur after the draw auctions have begun for the open draw since a claim might dip into + /// the reserve and cause auction results to be inaccurate. By limiting the claim period to 1 period after the + /// draw ends, we provide a predictable environment for draw auctions to operate in. + if (block.timestamp >= _openDrawEndsAt()) { + revert ClaimPeriodExpired(); + } if (_feeRecipient == address(0) && _fee > 0) { revert FeeRecipientZeroAddress(); } @@ -480,7 +490,7 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { uint256 amount; if (_fee != 0) { emit IncreaseClaimRewards(_feeRecipient, _fee); - _claimerRewards[_feeRecipient] += _fee; + _rewards[_feeRecipient] += _fee; unchecked { amount = tierLiquidity.prizeSize - _fee; @@ -511,23 +521,23 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { } /** - * @notice Withdraws the claim fees for the caller. - * @param _to The address to transfer the claim fees to. - * @param _amount The amount of claim fees to withdraw + * @notice Withdraws earned rewards for the caller. + * @param _to The address to transfer the rewards to + * @param _amount The amount of rewards to withdraw */ - function withdrawClaimRewards(address _to, uint256 _amount) external { - uint256 _available = _claimerRewards[msg.sender]; + function withdrawRewards(address _to, uint256 _amount) external { + uint256 _available = _rewards[msg.sender]; if (_amount > _available) { revert InsufficientRewardsError(_amount, _available); } unchecked { - _claimerRewards[msg.sender] = _available - _amount; + _rewards[msg.sender] = _available - _amount; } _transfer(_to, _amount); - emit WithdrawClaimRewards(_to, _amount, _available); + emit WithdrawRewards(msg.sender, _to, _amount, _available); } /// @notice Allows anyone to deposit directly into the Prize Pool reserve. @@ -686,12 +696,12 @@ contract PrizePool is TieredLiquidityDistributor, Ownable { } /** - * @notice Returns the balance of fees for a given claimer - * @param _claimer The claimer to retrieve the fee balance for - * @return The balance of fees for the given claimer + * @notice Returns the balance of rewards earned for the given address. + * @param _recipient The recipient to retrieve the reward balance for + * @return The balance of rewards for the given recipient */ - function balanceOfClaimRewards(address _claimer) external view returns (uint256) { - return _claimerRewards[_claimer]; + function rewardBalance(address _recipient) external view returns (uint256) { + return _rewards[_recipient]; } /** diff --git a/test/PrizePool.t.sol b/test/PrizePool.t.sol index d39331a..4ea55fe 100644 --- a/test/PrizePool.t.sol +++ b/test/PrizePool.t.sol @@ -14,7 +14,7 @@ import { TwabController } from "pt-v5-twab-controller/TwabController.sol"; import { TierCalculationLib } from "../src/libraries/TierCalculationLib.sol"; import { MAXIMUM_NUMBER_OF_TIERS, MINIMUM_NUMBER_OF_TIERS } from "../src/abstract/TieredLiquidityDistributor.sol"; -import { PrizePool, PrizeIsZero, ConstructorParams, InsufficientRewardsError, DidNotWin, FeeTooLarge, SmoothingGTEOne, ContributionGTDeltaBalance, InsufficientReserve, RandomNumberIsZero, DrawNotFinished, InvalidPrizeIndex, NoClosedDraw, InvalidTier, DrawManagerIsZeroAddress, CallerNotDrawManager, NotDeployer, FeeRecipientZeroAddress, FirstDrawStartsInPast, IncompatibleTwabPeriodLength, IncompatibleTwabPeriodOffset } from "../src/PrizePool.sol"; +import { PrizePool, PrizeIsZero, ConstructorParams, InsufficientRewardsError, DidNotWin, FeeTooLarge, SmoothingGTEOne, ContributionGTDeltaBalance, InsufficientReserve, RandomNumberIsZero, DrawNotFinished, InvalidPrizeIndex, NoClosedDraw, InvalidTier, DrawManagerIsZeroAddress, CallerNotDrawManager, NotDeployer, FeeRecipientZeroAddress, FirstDrawStartsInPast, IncompatibleTwabPeriodLength, IncompatibleTwabPeriodOffset, ClaimPeriodExpired } from "../src/PrizePool.sol"; import { ERC20Mintable } from "./mocks/ERC20Mintable.sol"; contract PrizePoolTest is Test { @@ -65,10 +65,15 @@ contract PrizePoolTest is Test { uint48 drawStartedAt ); - /// @notice Emitted when any amount of the reserve is withdrawn. - /// @param to The address the assets are transferred to + /// @notice Emitted when any amount of the reserve is rewarded to a recipient. + /// @param to The recipient of the reward + /// @param amount The amount of assets rewarded + event AllocateRewardFromReserve(address indexed to, uint256 amount); + + /// @notice Emitted when the reserve is manually increased. + /// @param user The user who increased the reserve /// @param amount The amount of assets transferred - event WithdrawReserve(address indexed to, uint256 amount); + event ContributedReserve(address indexed user, uint256 amount); /// @notice Emitted when a vault contributes prize tokens to the pool. /// @param vault The address of the vault that is contributing tokens @@ -76,26 +81,22 @@ contract PrizePoolTest is Test { /// @param amount The amount of tokens contributed event ContributePrizeTokens(address indexed vault, uint24 indexed drawId, uint256 amount); - /// @notice Emitted when an address withdraws their claim rewards + /// @notice Emitted when an address withdraws their prize claim rewards. + /// @param account The account that is withdrawing rewards /// @param to The address the rewards are sent to /// @param amount The amount withdrawn /// @param available The total amount that was available to withdraw before the transfer - event WithdrawClaimRewards(address indexed to, uint256 amount, uint256 available); + event WithdrawRewards(address indexed account, address indexed to, uint256 amount, uint256 available); - /// @notice Emitted when an address receives new claim rewards + /// @notice Emitted when an address receives new prize claim rewards. /// @param to The address the rewards are given to /// @param amount The amount increased event IncreaseClaimRewards(address indexed to, uint256 amount); - /// @notice Emitted when the drawManager is set + /// @notice Emitted when the drawManager is set. /// @param drawManager The draw manager event DrawManagerSet(address indexed drawManager); - /// @notice Emitted when the reserve is manually increased. - /// @param user The user who increased the reserve - /// @param amount The amount of assets transferred - event ContributedReserve(address indexed user, uint256 amount); - /**********************************************************************************/ ConstructorParams params; @@ -282,28 +283,35 @@ contract PrizePoolTest is Test { assertEq(prizePool.reserveForOpenDraw(), newReserve); } - function testWithdrawReserve_notManager() public { + function testAllocateRewardFromReserve_notManager() public { vm.prank(address(0)); vm.expectRevert( abi.encodeWithSelector(CallerNotDrawManager.selector, address(0), address(this)) ); - prizePool.withdrawReserve(address(0), 1); + prizePool.allocateRewardFromReserve(address(0), 1); } - function testWithdrawReserve_insuff() public { + function testAllocateRewardFromReserve_insuff() public { vm.expectRevert(abi.encodeWithSelector(InsufficientReserve.selector, 1, 0)); - prizePool.withdrawReserve(address(this), 1); + prizePool.allocateRewardFromReserve(address(this), 1); } - function testWithdrawReserve() public { + function testAllocateRewardFromReserve() public { contribute(310e18); closeDraw(winningRandomNumber); assertEq(prizeToken.balanceOf(address(this)), 0); vm.expectEmit(); - emit WithdrawReserve(address(this), 1e18); - prizePool.withdrawReserve(address(this), 1e18); - assertEq(prizeToken.balanceOf(address(this)), 1e18); - assertEq(prizePool.accountedBalance(), 309e18); + emit AllocateRewardFromReserve(address(this), 1e18); + prizePool.allocateRewardFromReserve(address(this), 1e18); + assertEq(prizePool.rewardBalance(address(this)), 1e18); + assertEq(prizeToken.balanceOf(address(this)), 0); // still 0 since there shouldn't be a transfer + assertEq(prizePool.accountedBalance(), 310e18); // still 310e18 since there were no tokens transferred out yet + + // withdraw rewards: + prizePool.withdrawRewards(address(this), 1e17); + assertEq(prizePool.rewardBalance(address(this)), 9e17); + assertEq(prizeToken.balanceOf(address(this)), 1e17); + assertEq(prizePool.accountedBalance(), 3099e17); } function testGetTotalContributedBetween() public { @@ -346,7 +354,7 @@ contract PrizePoolTest is Test { (10e18 * RESERVE_SHARES) / prizePool.getTotalShares(), 100 ); - prizePool.withdrawReserve(address(this), prizePool.reserve()); + prizePool.allocateRewardFromReserve(address(this), prizePool.reserve()); assertEq(prizePool.accountedBalance(), prizeToken.balanceOf(address(prizePool))); assertEq(prizePool.reserve(), 0); } @@ -847,6 +855,40 @@ contract PrizePoolTest is Test { prizePool.claimPrize(winner, 1, 0, winner, 0, address(this)); } + function testClaimPrize_ClaimPeriodExpired() public { + contribute(100e18); + closeDraw(winningRandomNumber); + address winner = makeAddr("winner"); + mockTwab(address(this), winner, 1); + uint periodStart = prizePool.openDrawStartedAt(); + + // warp to end of open draw (end of claim period) + vm.warp(periodStart + drawPeriodSeconds); + vm.expectRevert(abi.encodeWithSelector(ClaimPeriodExpired.selector)); + prizePool.claimPrize(winner, 1, 0, winner, 0, address(this)); + + // warp to end of open draw (end of claim period) + 1 sec + vm.warp(periodStart + drawPeriodSeconds + 1); + vm.expectRevert(abi.encodeWithSelector(ClaimPeriodExpired.selector)); + prizePool.claimPrize(winner, 1, 0, winner, 0, address(this)); + + // warp to right before end of open draw (end of claim period) + vm.warp(periodStart + drawPeriodSeconds - 1); + vm.expectEmit(); + emit ClaimedPrize( + address(this), + winner, + winner, + 1, + 1, + 0, + uint152(prizePool.getTierPrizeSize(1)), + 0, + address(this) + ); + prizePool.claimPrize(winner, 1, 0, winner, 0, address(this)); + } + function testClaimPrize_single() public { contribute(100e18); closeDraw(winningRandomNumber); @@ -878,7 +920,7 @@ contract PrizePoolTest is Test { // grand prize is (100/220) * 0.1 * 100e18 = 4.5454...e18 assertEq(prizeToken.balanceOf(msg.sender), prize - 1e18, "balance is prize less fee"); assertEq(prizePool.claimCount(), 1); - assertEq(prizePool.balanceOfClaimRewards(address(this)), 1e18); + assertEq(prizePool.rewardBalance(address(this)), 1e18); } function testClaimPrize_notWinner() public { @@ -986,6 +1028,57 @@ contract PrizePoolTest is Test { assertEq(prizePool.claimCount(), 1); } + function testClaimPrize_claimFeesAccountedFor() public { + contribute(100e18); + closeDraw(winningRandomNumber); + + address winner = makeAddr("winner"); + address recipient = makeAddr("recipient"); + mockTwab(address(this), winner, 1); + + uint96 fee = 0xfee; + uint prizeAmount = 806451612903225800; + uint prize = prizeAmount - fee; + assertApproxEqAbs(prizeAmount, (10e18 * TIER_SHARES) / (4 * prizePool.getTotalShares()), 100); + + vm.expectEmit(); + emit ClaimedPrize( + address(this), + winner, + recipient, + 1, + 1, + 0, + uint152(prize), + fee, + address(this) + ); + assertEq(prizePool.claimPrize(winner, 1, 0, recipient, fee, address(this)), prizeAmount); + assertEq(prizeToken.balanceOf(recipient), prize, "recipient balance is good"); + assertEq(prizePool.claimCount(), 1); + + // Check if claim fees are accounted for + // (if they aren't anyone can call contributePrizeTokens with the unaccounted fee amount and basically take it as their own) + uint accountedBalance = prizePool.accountedBalance(); + uint actualBalance = prizeToken.balanceOf(address(prizePool)); + console2.log("accounted balance: ", accountedBalance); + console2.log("actual balance: ", actualBalance); + console2.log("diff: ", actualBalance - accountedBalance); + + // show that the claimer can still withdraw their fees: + assertEq(prizeToken.balanceOf(address(this)), 0); + vm.expectEmit(); + emit WithdrawRewards(address(this), address(this), fee, fee); + prizePool.withdrawRewards(address(this), fee); + assertEq(prizeToken.balanceOf(address(this)), fee); + + accountedBalance = prizePool.accountedBalance(); + actualBalance = prizeToken.balanceOf(address(prizePool)); + console2.log("accounted balance: ", accountedBalance); + console2.log("actual balance: ", actualBalance); + console2.log("diff: ", actualBalance - accountedBalance); + } + function testTotalWithdrawn() public { assertEq(prizePool.totalWithdrawn(), 0); contribute(100e18); @@ -1047,21 +1140,21 @@ contract PrizePoolTest is Test { assertEq(prizePool.hasOpenDrawFinished(), true); } - function testWithdrawClaimRewards_sufficient() public { + function testWithdrawRewards_sufficient() public { contribute(100e18); closeDraw(winningRandomNumber); mockTwab(address(this), msg.sender, 0); claimPrize(msg.sender, 0, 0, 1e18, address(this)); - prizePool.withdrawClaimRewards(address(this), 1e18); + prizePool.withdrawRewards(address(this), 1e18); assertEq(prizeToken.balanceOf(address(this)), 1e18); } - function testWithdrawClaimRewards_insufficient() public { + function testWithdrawRewards_insufficient() public { vm.expectRevert(abi.encodeWithSelector(InsufficientRewardsError.selector, 1e18, 0)); - prizePool.withdrawClaimRewards(address(this), 1e18); + prizePool.withdrawRewards(address(this), 1e18); } - function testWithdrawClaimRewards_emitsEvent() public { + function testWithdrawRewards_emitsEvent() public { contribute(100e18); closeDraw(winningRandomNumber); mockTwab(address(this), msg.sender, 0); @@ -1069,8 +1162,8 @@ contract PrizePoolTest is Test { prizePool.claimPrize(msg.sender, 0, 0, msg.sender, 1e18, address(this)); vm.expectEmit(); - emit WithdrawClaimRewards(address(this), 5e17, 1e18); - prizePool.withdrawClaimRewards(address(this), 5e17); + emit WithdrawRewards(address(this), address(1), 5e17, 1e18); + prizePool.withdrawRewards(address(1), 5e17); } function testOpenDrawStartsAt_zeroDraw() public { diff --git a/test/invariants/PrizePoolInvariants.t.sol b/test/invariants/PrizePoolInvariants.t.sol index 078dd86..590f15c 100644 --- a/test/invariants/PrizePoolInvariants.t.sol +++ b/test/invariants/PrizePoolInvariants.t.sol @@ -15,7 +15,7 @@ contract PrizePoolInvariants is Test { bytes4[] memory selectors = new bytes4[](6); selectors[0] = prizePoolHarness.contributePrizeTokens.selector; selectors[1] = prizePoolHarness.contributeReserve.selector; - selectors[2] = prizePoolHarness.withdrawReserve.selector; + selectors[2] = prizePoolHarness.allocateRewardFromReserve.selector; selectors[3] = prizePoolHarness.withdrawClaimReward.selector; selectors[4] = prizePoolHarness.claimPrizes.selector; selectors[5] = prizePoolHarness.closeDraw.selector; diff --git a/test/invariants/helpers/PrizePoolFuzzHarness.sol b/test/invariants/helpers/PrizePoolFuzzHarness.sol index d52e8fa..bb782b8 100644 --- a/test/invariants/helpers/PrizePoolFuzzHarness.sol +++ b/test/invariants/helpers/PrizePoolFuzzHarness.sol @@ -70,15 +70,15 @@ contract PrizePoolFuzzHarness is CommonBase, StdCheats { prizePool.contributeReserve(_amount); } - function withdrawReserve() public warp { + function allocateRewardFromReserve() public warp { uint96 amount = prizePool.reserve(); withdrawn += amount; - prizePool.withdrawReserve(address(msg.sender), amount); + prizePool.allocateRewardFromReserve(address(msg.sender), amount); } function withdrawClaimReward() public warp { vm.startPrank(claimer); - prizePool.withdrawClaimRewards(address(claimer), prizePool.balanceOfClaimRewards(claimer)); + prizePool.withdrawRewards(address(claimer), prizePool.rewardBalance(claimer)); vm.stopPrank(); }