From 2b4cd6b2cf1d8e733a436188ea55fd33ea321796 Mon Sep 17 00:00:00 2001 From: Dylan DesRosier Date: Tue, 27 Jun 2023 20:29:27 +0100 Subject: [PATCH] Formatting --- src/PrizePool.sol | 1430 ++++++------ src/abstract/TieredLiquidityDistributor.sol | 1164 ++++++---- src/libraries/BitLib.sol | 39 +- src/libraries/DrawAccumulatorLib.sol | 683 +++--- src/libraries/TierCalculationLib.sol | 238 +- src/libraries/UD34x4.sol | 32 +- test/PrizePool.t.sol | 1960 +++++++++-------- .../TieredLiquidityDistributorWrapper.sol | 66 +- .../DrawAccumulatorInvariants.t.sol | 17 +- test/invariants/PrizePoolInvariants.t.sol | 21 +- .../TierCalculationInvariants.t.sol | 52 +- ...TieredLiquidityDistributorInvariants.t.sol | 81 +- .../helpers/DrawAccumulatorFuzzHarness.sol | 37 +- .../helpers/PrizePoolFuzzHarness.sol | 127 +- .../helpers/TierCalculationFuzzHarness.sol | 75 +- .../TieredLiquidityDistributorFuzzHarness.sol | 92 +- test/libraries/BitLib.t.sol | 88 +- test/libraries/DrawAccumulatorLib.t.sol | 489 ++-- test/libraries/TierCalculationLib.t.sol | 266 +-- test/libraries/UD34x4.t.sol | 84 +- test/mocks/ERC20Mintable.sol | 5 +- test/wrappers/BitLibWrapper.sol | 35 +- test/wrappers/DrawAccumulatorLibWrapper.sol | 152 +- test/wrappers/TierCalculationLibWrapper.sol | 32 +- 24 files changed, 3913 insertions(+), 3352 deletions(-) diff --git a/src/PrizePool.sol b/src/PrizePool.sol index 83aca2a..6c5f62d 100644 --- a/src/PrizePool.sol +++ b/src/PrizePool.sol @@ -69,7 +69,7 @@ error WinnerPrizeMismatch(uint128 numWinners, uint128 numPrizeLists); /// @notice Emitted when prize index is greater or equal to the max prize count for the tier /// @param invalidPrizeIndex The invalid prize index /// @param prizeCount The prize count for the tier -/// @param tier The tier number +/// @param tier The tier number error InvalidPrizeIndex(uint32 invalidPrizeIndex, uint32 prizeCount, uint8 tier); /// @notice Emitted when there are no completed draws when a computation requires a completed draw @@ -101,18 +101,18 @@ error CallerNotDrawManager(address caller, address drawManager); * @param smoothing The amount of smoothing to apply to vault contributions. Must be less than 1. A value of 0 is no smoothing, while greater values smooth until approaching infinity */ struct ConstructorParams { - IERC20 prizeToken; - TwabController twabController; - address drawManager; - uint16 grandPrizePeriodDraws; - uint32 drawPeriodSeconds; - uint64 firstDrawStartsAt; - uint8 numberOfTiers; - uint8 tierShares; - uint8 canaryShares; - uint8 reserveShares; - UD2x18 claimExpansionThreshold; - SD1x18 smoothing; + IERC20 prizeToken; + TwabController twabController; + address drawManager; + uint16 grandPrizePeriodDraws; + uint32 drawPeriodSeconds; + uint64 firstDrawStartsAt; + uint8 numberOfTiers; + uint8 tierShares; + uint8 canaryShares; + uint8 reserveShares; + UD2x18 claimExpansionThreshold; + SD1x18 smoothing; } /** @@ -121,384 +121,443 @@ struct ConstructorParams { * @notice The Prize Pool holds the prize liquidity and allows vaults to claim prizes. */ contract PrizePool is TieredLiquidityDistributor { - using SafeERC20 for IERC20; - - /// @notice Emitted when a prize is claimed. - /// @param drawId The draw ID of the draw that was claimed. - /// @param vault The address of the vault that claimed the prize. - /// @param winner The address of the winner - /// @param tier The prize tier that was claimed. - /// @param payout The amount of prize tokens that were paid out to the winner - /// @param fee The amount of prize tokens that were paid to the claimer - /// @param feeRecipient The address that the claim fee was sent to - event ClaimedPrize( - uint16 indexed drawId, - address indexed vault, - address indexed winner, - uint8 tier, - uint152 payout, - uint96 fee, - address feeRecipient + using SafeERC20 for IERC20; + + /// @notice Emitted when a prize is claimed. + /// @param drawId The draw ID of the draw that was claimed. + /// @param vault The address of the vault that claimed the prize. + /// @param winner The address of the winner + /// @param tier The prize tier that was claimed. + /// @param payout The amount of prize tokens that were paid out to the winner + /// @param fee The amount of prize tokens that were paid to the claimer + /// @param feeRecipient The address that the claim fee was sent to + event ClaimedPrize( + uint16 indexed drawId, + address indexed vault, + address indexed winner, + uint8 tier, + uint152 payout, + uint96 fee, + address feeRecipient + ); + + /// @notice Emitted when a draw is completed. + /// @param drawId The ID of the draw that was completed + /// @param winningRandomNumber The winning random number for the completed draw + /// @param numTiers The number of prize tiers in the completed draw + /// @param nextNumTiers The number of tiers for the next draw + event DrawCompleted( + uint16 indexed drawId, + uint256 winningRandomNumber, + uint8 numTiers, + uint8 nextNumTiers + ); + + /// @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 a vault contributes prize tokens to the pool. + /// @param vault The address of the vault that is contributing tokens + /// @param drawId The ID of the first draw that the tokens will be applied to + /// @param amount The amount of tokens contributed + event ContributePrizeTokens(address indexed vault, uint16 indexed drawId, uint256 amount); + + /// @notice Emitted when an address withdraws their claim 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); + + /// @notice Emitted when the drawManager is set + /// @param drawManager The draw manager + event DrawManagerSet(address indexed drawManager); + + /// @notice The DrawAccumulator that tracks the exponential moving average of the contributions by a vault + mapping(address => DrawAccumulatorLib.Accumulator) internal vaultAccumulator; + + /// @notice Records the claim record for a winner + /// @dev account => drawId => tier => prizeIndex => claimed + mapping(address => mapping(uint16 => mapping(uint8 => mapping(uint32 => bool)))) + internal claimedPrizes; + + /// @notice Tracks the total fees accrued to each claimer + mapping(address => uint256) internal claimerRewards; + + /// @notice The degree of POOL contribution smoothing. 0 = no smoothing, ~1 = max smoothing. Smoothing spreads out vault contribution over multiple draws; the higher the smoothing the more draws. + SD1x18 public immutable smoothing; + + /// @notice The token that is being contributed and awarded as prizes + IERC20 public immutable prizeToken; + + /// @notice The Twab Controller to use to retrieve historic balances. + TwabController public immutable twabController; + + /// @notice The draw manager address + address public drawManager; + + /// @notice The number of seconds between draws + uint32 public immutable drawPeriodSeconds; + + // percentage of prizes that must be claimed to bump the number of tiers + // 64 bits + UD2x18 public immutable claimExpansionThreshold; + + /// @notice The exponential weighted average of all vault contributions + DrawAccumulatorLib.Accumulator internal totalAccumulator; + + uint256 internal _totalWithdrawn; + + /// @notice The winner random number for the last completed draw + uint256 internal _winningRandomNumber; + + /// @notice The number of prize claims for the last completed draw + uint32 public claimCount; + + /// @notice The number of canary prize claims for the last completed draw + uint32 public canaryClaimCount; + + /// @notice The largest tier claimed so far for the last completed draw + uint8 public largestTierClaimed; + + /// @notice The timestamp at which the last completed draw started + uint64 internal _lastCompletedDrawStartedAt; + + /// @notice The timestamp at which the last completed draw was awarded + uint64 internal _lastCompletedDrawAwardedAt; + + /// @notice Constructs a new Prize Pool + /// @param params A struct of constructor parameters + constructor( + ConstructorParams memory params + ) + TieredLiquidityDistributor( + params.grandPrizePeriodDraws, + params.numberOfTiers, + params.tierShares, + params.canaryShares, + params.reserveShares + ) + { + if (unwrap(params.smoothing) >= unwrap(UNIT)) { + revert SmoothingGTEOne(unwrap(params.smoothing)); + } + prizeToken = params.prizeToken; + twabController = params.twabController; + smoothing = params.smoothing; + claimExpansionThreshold = params.claimExpansionThreshold; + drawPeriodSeconds = params.drawPeriodSeconds; + _lastCompletedDrawStartedAt = params.firstDrawStartsAt; + + drawManager = params.drawManager; + if (params.drawManager != address(0)) { + emit DrawManagerSet(params.drawManager); + } + } + + /// @notice Modifier that throws if sender is not the draw manager + modifier onlyDrawManager() { + if (msg.sender != drawManager) { + revert CallerNotDrawManager(msg.sender, drawManager); + } + _; + } + + /// @notice Allows a caller to set the DrawManager if not already set + /// @dev Notice that this can be front-run: make sure to verify the drawManager after construction + /// @param _drawManager The draw manager + function setDrawManager(address _drawManager) external { + if (drawManager != address(0)) { + revert DrawManagerAlreadySet(); + } + drawManager = _drawManager; + + emit DrawManagerSet(_drawManager); + } + + /// @notice Returns the winning random number for the last completed draw + /// @return The winning random number + function getWinningRandomNumber() external view returns (uint256) { + return _winningRandomNumber; + } + + /// @notice Returns the last completed draw id + /// @return The last completed draw id + function getLastCompletedDrawId() external view returns (uint256) { + return lastCompletedDrawId; + } + + /// @notice Returns the total prize tokens contributed between the given draw ids, inclusive. Note that this is after smoothing is applied. + /// @return The total prize tokens contributed by all vaults + function getTotalContributedBetween( + uint16 _startDrawIdInclusive, + uint16 _endDrawIdInclusive + ) external view returns (uint256) { + return + DrawAccumulatorLib.getDisbursedBetween( + totalAccumulator, + _startDrawIdInclusive, + _endDrawIdInclusive, + smoothing.intoSD59x18() + ); + } + + /// @notice Returns the total prize tokens contributed by a particular vault between the given draw ids, inclusive. Note that this is after smoothing is applied. + /// @return The total prize tokens contributed by the given vault + function getContributedBetween( + address _vault, + uint16 _startDrawIdInclusive, + uint16 _endDrawIdInclusive + ) external view returns (uint256) { + return + DrawAccumulatorLib.getDisbursedBetween( + vaultAccumulator[_vault], + _startDrawIdInclusive, + _endDrawIdInclusive, + smoothing.intoSD59x18() + ); + } + + /// @notice Returns the + /// @return The number of draws + function getTierAccrualDurationInDraws(uint8 _tier) external view returns (uint16) { + return + uint16( + TierCalculationLib.estimatePrizeFrequencyInDraws( + _tier, + numberOfTiers, + grandPrizePeriodDraws + ) + ); + } + + /// @notice Contributes prize tokens on behalf of the given vault. The tokens should have already been transferred to the prize pool. + /// The prize pool balance will be checked to ensure there is at least the given amount to deposit. + /// @return The amount of available prize tokens prior to the contribution. + function contributePrizeTokens(address _prizeVault, uint256 _amount) external returns (uint256) { + uint256 _deltaBalance = prizeToken.balanceOf(address(this)) - _accountedBalance(); + if (_deltaBalance < _amount) { + revert ContributionGTDeltaBalance(_amount, _deltaBalance); + } + DrawAccumulatorLib.add( + vaultAccumulator[_prizeVault], + _amount, + lastCompletedDrawId + 1, + smoothing.intoSD59x18() ); - - /// @notice Emitted when a draw is completed. - /// @param drawId The ID of the draw that was completed - /// @param winningRandomNumber The winning random number for the completed draw - /// @param numTiers The number of prize tiers in the completed draw - /// @param nextNumTiers The number of tiers for the next draw - event DrawCompleted( - uint16 indexed drawId, - uint256 winningRandomNumber, - uint8 numTiers, - uint8 nextNumTiers + DrawAccumulatorLib.add( + totalAccumulator, + _amount, + lastCompletedDrawId + 1, + smoothing.intoSD59x18() ); - - /// @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 + emit ContributePrizeTokens(_prizeVault, lastCompletedDrawId + 1, _amount); + return _deltaBalance; + } + + /// @notice Computes how many tokens have been accounted for + /// @return The balance of tokens that have been accounted for + function _accountedBalance() internal view returns (uint256) { + Observation memory obs = DrawAccumulatorLib.newestObservation(totalAccumulator); + return (obs.available + obs.disbursed) - _totalWithdrawn; + } + + /// @notice The total amount of prize tokens that have been claimed for all time + /// @return The total amount of prize tokens that have been claimed for all time + function totalWithdrawn() external view returns (uint256) { + return _totalWithdrawn; + } + + /// @notice Computes how many tokens have been accounted for + /// @return The balance of tokens that have been accounted for + function accountedBalance() external view returns (uint256) { + return _accountedBalance(); + } + + /// @notice Returns the start time of the last completed draw. If there was no completed draw, then it will be zero. + /// @return The start time of the last completed draw + function lastCompletedDrawStartedAt() external view returns (uint64) { + return lastCompletedDrawId != 0 ? _lastCompletedDrawStartedAt : 0; + } + + /// @notice Returns the end time of the last completed draw. If there was no completed draw, then it will be zero. + /// @return The end time of the last completed draw + function lastCompletedDrawEndedAt() external view returns (uint64) { + return lastCompletedDrawId != 0 ? _lastCompletedDrawStartedAt + drawPeriodSeconds : 0; + } + + /// @notice Returns the time at which the last completed draw was awarded. + /// @return The time at which the last completed draw was awarded + function lastCompletedDrawAwardedAt() external view returns (uint64) { + return lastCompletedDrawId != 0 ? _lastCompletedDrawAwardedAt : 0; + } + + // @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, uint104 _amount) external onlyDrawManager { + if (_amount > _reserve) { + revert InsufficientReserve(_amount, _reserve); + } + _reserve -= _amount; + _transfer(_to, _amount); + emit WithdrawReserve(_to, _amount); + } + + /// @notice Returns whether the next draw has finished + function hasNextDrawFinished() external view returns (bool) { + return block.timestamp >= _nextDrawEndsAt(); + } + + /// @notice Returns the start time of the draw for the next successful completeAndStartNextDraw + function nextDrawStartsAt() external view returns (uint64) { + return _nextDrawStartsAt(); + } + + /// @notice Returns the time at which the next draw ends + function nextDrawEndsAt() external view returns (uint64) { + return _nextDrawEndsAt(); + } + + /// @notice Returns the start time of the draw for the next successful completeAndStartNextDraw + function _nextDrawStartsAt() internal view returns (uint64) { + return _nextDrawEndsAt() - drawPeriodSeconds; + } + + /// @notice Returns the time at which the next draw end. + function _nextDrawEndsAt() internal view returns (uint64) { + // If this is the first draw, we treat _lastCompletedDrawStartedAt as the start of this draw + uint64 _nextExpectedEndTime = _lastCompletedDrawStartedAt + + (lastCompletedDrawId == 0 ? 1 : 2) * + drawPeriodSeconds; + + if (block.timestamp > _nextExpectedEndTime) { + // Use integer division to get the number of draw periods passed between the expected end time and now + // Offset the end time by the total duration of the missed draws + // drawPeriodSeconds * numMissedDraws + _nextExpectedEndTime += + drawPeriodSeconds * + (uint64((block.timestamp - _nextExpectedEndTime) / drawPeriodSeconds)); + } + + return _nextExpectedEndTime; + } + + function _computeNextNumberOfTiers(uint8 _numTiers) internal view returns (uint8) { + UD2x18 _claimExpansionThreshold = claimExpansionThreshold; + + uint8 _nextNumberOfTiers = largestTierClaimed + 2; // canary tier, then length + _nextNumberOfTiers = _nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS + ? _nextNumberOfTiers + : MINIMUM_NUMBER_OF_TIERS; + + // check to see if we need to expand the number of tiers + if (_nextNumberOfTiers >= _numTiers) { + if ( + canaryClaimCount >= + fromUD60x18( + intoUD60x18(_claimExpansionThreshold).mul(_canaryPrizeCountFractional(_numTiers).floor()) + ) && + claimCount >= + fromUD60x18( + intoUD60x18(_claimExpansionThreshold).mul(toUD60x18(_estimatedPrizeCount(_numTiers))) + ) + ) { + // increase the number of tiers to include a new tier + _nextNumberOfTiers = _numTiers + 1; + } + } + + return _nextNumberOfTiers; + } + + /// @notice Allows the Manager to complete the current prize period and starts the next one, updating the number of tiers, the winning random number, and the prize pool reserve + /// @param winningRandomNumber_ The winning random number for the current draw + /// @return The ID of the completed draw + function completeAndStartNextDraw( + uint256 winningRandomNumber_ + ) external onlyDrawManager returns (uint16) { + // check winning random number + if (winningRandomNumber_ == 0) { + revert RandomNumberIsZero(); + } + if (block.timestamp < _nextDrawEndsAt()) { + revert DrawNotFinished(_nextDrawEndsAt()); + } + + uint8 _numTiers = numberOfTiers; + uint8 _nextNumberOfTiers = _numTiers; + + if (lastCompletedDrawId != 0) { + _nextNumberOfTiers = _computeNextNumberOfTiers(_numTiers); + } + + uint64 nextDrawStartsAt_ = _nextDrawStartsAt(); + + _nextDraw(_nextNumberOfTiers, uint96(_contributionsForDraw(lastCompletedDrawId + 1))); + + _winningRandomNumber = winningRandomNumber_; + claimCount = 0; + canaryClaimCount = 0; + largestTierClaimed = 0; + _lastCompletedDrawStartedAt = nextDrawStartsAt_; + _lastCompletedDrawAwardedAt = uint64(block.timestamp); + + emit DrawCompleted(lastCompletedDrawId, winningRandomNumber_, _numTiers, _nextNumberOfTiers); + + return lastCompletedDrawId; + } + + /// @notice Returns the amount of tokens that will be added to the reserve on the next draw. + /// @dev Intended for Draw manager to use after the draw has ended but not yet been completed. + /// @return The amount of prize tokens that will be added to the reserve + function reserveForNextDraw() external view returns (uint256) { + uint8 _numTiers = numberOfTiers; + uint8 _nextNumberOfTiers = _numTiers; + + if (lastCompletedDrawId != 0) { + _nextNumberOfTiers = _computeNextNumberOfTiers(_numTiers); + } + + (, uint104 newReserve, ) = _computeNewDistributions( + _numTiers, + _nextNumberOfTiers, + uint96(_contributionsForDraw(lastCompletedDrawId + 1)) ); - /// @notice Emitted when a vault contributes prize tokens to the pool. - /// @param vault The address of the vault that is contributing tokens - /// @param drawId The ID of the first draw that the tokens will be applied to - /// @param amount The amount of tokens contributed - event ContributePrizeTokens( - address indexed vault, - uint16 indexed drawId, - uint256 amount - ); - - /// @notice Emitted when an address withdraws their claim 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 - ); - - /// @notice Emitted when the drawManager is set - /// @param drawManager The draw manager - event DrawManagerSet( - address indexed drawManager - ); - - /// @notice The DrawAccumulator that tracks the exponential moving average of the contributions by a vault - mapping(address => DrawAccumulatorLib.Accumulator) internal vaultAccumulator; - - /// @notice Records the claim record for a winner - /// @dev account => drawId => tier => prizeIndex => claimed - mapping(address => mapping(uint16 => mapping(uint8 => mapping(uint32 => bool)))) internal claimedPrizes; - - /// @notice Tracks the total fees accrued to each claimer - mapping(address => uint256) internal claimerRewards; - - /// @notice The degree of POOL contribution smoothing. 0 = no smoothing, ~1 = max smoothing. Smoothing spreads out vault contribution over multiple draws; the higher the smoothing the more draws. - SD1x18 public immutable smoothing; - - /// @notice The token that is being contributed and awarded as prizes - IERC20 public immutable prizeToken; - - /// @notice The Twab Controller to use to retrieve historic balances. - TwabController public immutable twabController; - - /// @notice The draw manager address - address public drawManager; - - /// @notice The number of seconds between draws - uint32 public immutable drawPeriodSeconds; - - // percentage of prizes that must be claimed to bump the number of tiers - // 64 bits - UD2x18 public immutable claimExpansionThreshold; - - /// @notice The exponential weighted average of all vault contributions - DrawAccumulatorLib.Accumulator internal totalAccumulator; - - uint256 internal _totalWithdrawn; - - /// @notice The winner random number for the last completed draw - uint256 internal _winningRandomNumber; - - /// @notice The number of prize claims for the last completed draw - uint32 public claimCount; - - /// @notice The number of canary prize claims for the last completed draw - uint32 public canaryClaimCount; - - /// @notice The largest tier claimed so far for the last completed draw - uint8 public largestTierClaimed; - - /// @notice The timestamp at which the last completed draw started - uint64 internal _lastCompletedDrawStartedAt; - - /// @notice The timestamp at which the last completed draw was awarded - uint64 internal _lastCompletedDrawAwardedAt; - - /// @notice Constructs a new Prize Pool - /// @param params A struct of constructor parameters - constructor ( - ConstructorParams memory params - ) TieredLiquidityDistributor(params.grandPrizePeriodDraws, params.numberOfTiers, params.tierShares, params.canaryShares, params.reserveShares) { - if(unwrap(params.smoothing) >= unwrap(UNIT)) { - revert SmoothingGTEOne(unwrap(params.smoothing)); - } - prizeToken = params.prizeToken; - twabController = params.twabController; - smoothing = params.smoothing; - claimExpansionThreshold = params.claimExpansionThreshold; - drawPeriodSeconds = params.drawPeriodSeconds; - _lastCompletedDrawStartedAt = params.firstDrawStartsAt; - - drawManager = params.drawManager; - if (params.drawManager != address(0)) { - emit DrawManagerSet(params.drawManager); - } - } - - /// @notice Modifier that throws if sender is not the draw manager - modifier onlyDrawManager() { - if(msg.sender != drawManager) { - revert CallerNotDrawManager(msg.sender, drawManager); - } - _; - } - - /// @notice Allows a caller to set the DrawManager if not already set - /// @dev Notice that this can be front-run: make sure to verify the drawManager after construction - /// @param _drawManager The draw manager - function setDrawManager(address _drawManager) external { - if (drawManager != address(0)) { - revert DrawManagerAlreadySet(); - } - drawManager = _drawManager; - - emit DrawManagerSet(_drawManager); - } - - /// @notice Returns the winning random number for the last completed draw - /// @return The winning random number - function getWinningRandomNumber() external view returns (uint256) { - return _winningRandomNumber; - } - - /// @notice Returns the last completed draw id - /// @return The last completed draw id - function getLastCompletedDrawId() external view returns (uint256) { - return lastCompletedDrawId; - } - - /// @notice Returns the total prize tokens contributed between the given draw ids, inclusive. Note that this is after smoothing is applied. - /// @return The total prize tokens contributed by all vaults - function getTotalContributedBetween(uint16 _startDrawIdInclusive, uint16 _endDrawIdInclusive) external view returns (uint256) { - return DrawAccumulatorLib.getDisbursedBetween(totalAccumulator, _startDrawIdInclusive, _endDrawIdInclusive, smoothing.intoSD59x18()); - } - - /// @notice Returns the total prize tokens contributed by a particular vault between the given draw ids, inclusive. Note that this is after smoothing is applied. - /// @return The total prize tokens contributed by the given vault - function getContributedBetween(address _vault, uint16 _startDrawIdInclusive, uint16 _endDrawIdInclusive) external view returns (uint256) { - return DrawAccumulatorLib.getDisbursedBetween(vaultAccumulator[_vault], _startDrawIdInclusive, _endDrawIdInclusive, smoothing.intoSD59x18()); - } - - /// @notice Returns the - /// @return The number of draws - function getTierAccrualDurationInDraws(uint8 _tier) external view returns (uint16) { - return uint16(TierCalculationLib.estimatePrizeFrequencyInDraws(_tier, numberOfTiers, grandPrizePeriodDraws)); - } - - /// @notice Contributes prize tokens on behalf of the given vault. The tokens should have already been transferred to the prize pool. - /// The prize pool balance will be checked to ensure there is at least the given amount to deposit. - /// @return The amount of available prize tokens prior to the contribution. - function contributePrizeTokens(address _prizeVault, uint256 _amount) external returns(uint256) { - uint256 _deltaBalance = prizeToken.balanceOf(address(this)) - _accountedBalance(); - if(_deltaBalance < _amount) { - revert ContributionGTDeltaBalance(_amount, _deltaBalance); - } - DrawAccumulatorLib.add(vaultAccumulator[_prizeVault], _amount, lastCompletedDrawId + 1, smoothing.intoSD59x18()); - DrawAccumulatorLib.add(totalAccumulator, _amount, lastCompletedDrawId + 1, smoothing.intoSD59x18()); - emit ContributePrizeTokens(_prizeVault, lastCompletedDrawId + 1, _amount); - return _deltaBalance; - } - - /// @notice Computes how many tokens have been accounted for - /// @return The balance of tokens that have been accounted for - function _accountedBalance() internal view returns (uint256) { - Observation memory obs = DrawAccumulatorLib.newestObservation(totalAccumulator); - return (obs.available + obs.disbursed) - _totalWithdrawn; - } - - /// @notice The total amount of prize tokens that have been claimed for all time - /// @return The total amount of prize tokens that have been claimed for all time - function totalWithdrawn() external view returns (uint256) { - return _totalWithdrawn; - } - - /// @notice Computes how many tokens have been accounted for - /// @return The balance of tokens that have been accounted for - function accountedBalance() external view returns (uint256) { - return _accountedBalance(); - } - - /// @notice Returns the start time of the last completed draw. If there was no completed draw, then it will be zero. - /// @return The start time of the last completed draw - function lastCompletedDrawStartedAt() external view returns (uint64) { - return lastCompletedDrawId != 0 ? _lastCompletedDrawStartedAt : 0; - } - - /// @notice Returns the end time of the last completed draw. If there was no completed draw, then it will be zero. - /// @return The end time of the last completed draw - function lastCompletedDrawEndedAt() external view returns (uint64) { - return lastCompletedDrawId != 0 ? _lastCompletedDrawStartedAt + drawPeriodSeconds : 0; - } - - /// @notice Returns the time at which the last completed draw was awarded. - /// @return The time at which the last completed draw was awarded - function lastCompletedDrawAwardedAt() external view returns (uint64) { - return lastCompletedDrawId != 0 ? _lastCompletedDrawAwardedAt : 0; - } - - // @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, uint104 _amount) external onlyDrawManager { - if(_amount > _reserve) { - revert InsufficientReserve(_amount, _reserve); - } - _reserve -= _amount; - _transfer(_to, _amount); - emit WithdrawReserve(_to, _amount); - } - - /// @notice Returns whether the next draw has finished - function hasNextDrawFinished() external view returns (bool) { - return block.timestamp >= _nextDrawEndsAt(); - } - - /// @notice Returns the start time of the draw for the next successful completeAndStartNextDraw - function nextDrawStartsAt() external view returns (uint64) { - return _nextDrawStartsAt(); - } - - /// @notice Returns the time at which the next draw ends - function nextDrawEndsAt() external view returns (uint64) { - return _nextDrawEndsAt(); - } - - /// @notice Returns the start time of the draw for the next successful completeAndStartNextDraw - function _nextDrawStartsAt() internal view returns (uint64) { - return _nextDrawEndsAt() - drawPeriodSeconds; - } - - /// @notice Returns the time at which the next draw end. - function _nextDrawEndsAt() internal view returns (uint64) { - // If this is the first draw, we treat _lastCompletedDrawStartedAt as the start of this draw - uint64 _nextExpectedEndTime = _lastCompletedDrawStartedAt + (lastCompletedDrawId == 0 ? 1 : 2) * drawPeriodSeconds; - - if (block.timestamp > _nextExpectedEndTime) { - // Use integer division to get the number of draw periods passed between the expected end time and now - // Offset the end time by the total duration of the missed draws - // drawPeriodSeconds * numMissedDraws - _nextExpectedEndTime += drawPeriodSeconds * (uint64((block.timestamp - _nextExpectedEndTime) / drawPeriodSeconds)); - } - - return _nextExpectedEndTime; - } - - function _computeNextNumberOfTiers(uint8 _numTiers) internal view returns (uint8) { - UD2x18 _claimExpansionThreshold = claimExpansionThreshold; - - uint8 _nextNumberOfTiers = largestTierClaimed + 2; // canary tier, then length - _nextNumberOfTiers = _nextNumberOfTiers > MINIMUM_NUMBER_OF_TIERS ? _nextNumberOfTiers : MINIMUM_NUMBER_OF_TIERS; - - // check to see if we need to expand the number of tiers - if (_nextNumberOfTiers >= _numTiers) { - if ( - canaryClaimCount >= fromUD60x18(intoUD60x18(_claimExpansionThreshold).mul(_canaryPrizeCountFractional(_numTiers).floor()))&& - claimCount >= fromUD60x18(intoUD60x18(_claimExpansionThreshold).mul(toUD60x18(_estimatedPrizeCount(_numTiers)))) - ) { - // increase the number of tiers to include a new tier - _nextNumberOfTiers = _numTiers + 1; - } - } - - - return _nextNumberOfTiers; - } - - /// @notice Allows the Manager to complete the current prize period and starts the next one, updating the number of tiers, the winning random number, and the prize pool reserve - /// @param winningRandomNumber_ The winning random number for the current draw - /// @return The ID of the completed draw - function completeAndStartNextDraw(uint256 winningRandomNumber_) external onlyDrawManager returns (uint16) { - // check winning random number - if (winningRandomNumber_ == 0) { - revert RandomNumberIsZero(); - } - if (block.timestamp < _nextDrawEndsAt()) { - revert DrawNotFinished(_nextDrawEndsAt()); - } - - uint8 _numTiers = numberOfTiers; - uint8 _nextNumberOfTiers = _numTiers; - - if (lastCompletedDrawId != 0) { - _nextNumberOfTiers = _computeNextNumberOfTiers(_numTiers); - } - - uint64 nextDrawStartsAt_ = _nextDrawStartsAt(); - - _nextDraw(_nextNumberOfTiers, uint96(_contributionsForDraw(lastCompletedDrawId + 1))); - - _winningRandomNumber = winningRandomNumber_; - claimCount = 0; - canaryClaimCount = 0; - largestTierClaimed = 0; - _lastCompletedDrawStartedAt = nextDrawStartsAt_; - _lastCompletedDrawAwardedAt = uint64(block.timestamp); - - emit DrawCompleted(lastCompletedDrawId, winningRandomNumber_, _numTiers, _nextNumberOfTiers); - - return lastCompletedDrawId; - } - - /// @notice Returns the amount of tokens that will be added to the reserve on the next draw. - /// @dev Intended for Draw manager to use after the draw has ended but not yet been completed. - /// @return The amount of prize tokens that will be added to the reserve - function reserveForNextDraw() external view returns (uint256) { - uint8 _numTiers = numberOfTiers; - uint8 _nextNumberOfTiers = _numTiers; - - if (lastCompletedDrawId != 0) { - _nextNumberOfTiers = _computeNextNumberOfTiers(_numTiers); - } - - (, uint104 newReserve, ) = _computeNewDistributions(_numTiers, _nextNumberOfTiers, uint96(_contributionsForDraw(lastCompletedDrawId+1))); - - return newReserve; - } - - /// @notice Computes the tokens to be disbursed from the accumulator for a given draw. - function _contributionsForDraw(uint16 _drawId) internal view returns (uint256) { - return DrawAccumulatorLib.getDisbursedBetween(totalAccumulator, _drawId, _drawId, smoothing.intoSD59x18()); - } - - /// @notice Calculates the total liquidity available for the current completed draw. - function getTotalContributionsForCompletedDraw() external view returns (uint256) { - return _contributionsForDraw(lastCompletedDrawId); - } - - /// @notice Returns whether the winner has claimed the tier for the last completed draw - /// @param _winner The account to check - /// @param _tier The tier to check - /// @return True if the winner claimed the tier for the current draw, false otherwise. - function wasClaimed(address _winner, uint8 _tier, uint32 _prizeIndex) external view returns (bool) { - return claimedPrizes[_winner][lastCompletedDrawId][_tier][_prizeIndex]; - } - - /** + return newReserve; + } + + /// @notice Computes the tokens to be disbursed from the accumulator for a given draw. + function _contributionsForDraw(uint16 _drawId) internal view returns (uint256) { + return + DrawAccumulatorLib.getDisbursedBetween( + totalAccumulator, + _drawId, + _drawId, + smoothing.intoSD59x18() + ); + } + + /// @notice Calculates the total liquidity available for the current completed draw. + function getTotalContributionsForCompletedDraw() external view returns (uint256) { + return _contributionsForDraw(lastCompletedDrawId); + } + + /// @notice Returns whether the winner has claimed the tier for the last completed draw + /// @param _winner The account to check + /// @param _tier The tier to check + /// @return True if the winner claimed the tier for the current draw, false otherwise. + function wasClaimed( + address _winner, + uint8 _tier, + uint32 _prizeIndex + ) external view returns (bool) { + return claimedPrizes[_winner][lastCompletedDrawId][_tier][_prizeIndex]; + } + + /** @dev Claims a prize for a given winner and tier. This function takes in an address _winner, a uint8 _tier, a uint96 _fee, and an address _feeRecipient. It checks if _winner is actually the winner of the _tier for the calling vault. @@ -516,259 +575,364 @@ contract PrizePool is TieredLiquidityDistributor { @param _feeRecipient The address to receive the fee. @return Total prize amount claimed (payout and fees combined). */ - function claimPrizes( - uint8 _tier, - address[] calldata _winners, - uint32[][] calldata _prizeIndices, - uint96 _feePerPrizeClaim, - address _feeRecipient - ) external returns (uint256) { - if (_winners.length != _prizeIndices.length) { - revert WinnerPrizeMismatch(uint128(_winners.length), uint128(_prizeIndices.length)); - } - - Tier memory tierLiquidity = _getTier(_tier, numberOfTiers); - if (_feePerPrizeClaim > tierLiquidity.prizeSize) { - revert FeeTooLarge(_feePerPrizeClaim, tierLiquidity.prizeSize); - } - - uint32 prizeClaimCount = _claimPrizes(msg.sender, _tier, _winners, _prizeIndices, tierLiquidity.prizeSize - _feePerPrizeClaim, _feePerPrizeClaim, _feeRecipient); - - if (_isCanaryTier(_tier, numberOfTiers)) { - canaryClaimCount += prizeClaimCount; - } else { - claimCount += prizeClaimCount; - } - - if (largestTierClaimed < _tier) { - largestTierClaimed = _tier; - } - - _consumeLiquidity(tierLiquidity, _tier, tierLiquidity.prizeSize * prizeClaimCount); - - if (_feePerPrizeClaim != 0 && prizeClaimCount != 0) { - claimerRewards[_feeRecipient] += _feePerPrizeClaim * prizeClaimCount; - } - - return tierLiquidity.prizeSize * prizeClaimCount; - } - - function _claimPrizes( - address _vault, - uint8 _tier, - address[] calldata _winners, - uint32[][] calldata _prizeIndices, - uint96 _payout, - uint96 _feePerPrizeClaim, - address _feeRecipient - ) internal returns (uint32) { - uint32 _prizeClaimCount = 0; - (SD59x18 _vaultPortion, SD59x18 _tierOdds, uint16 _drawDuration) = _computeVaultTierDetails(_vault, _tier, numberOfTiers, lastCompletedDrawId); - - for (uint winnerIndex = 0; winnerIndex < _winners.length; winnerIndex++) { - _prizeClaimCount += _claimWinnerPrizes(_vault, _tier, _winners[winnerIndex], _prizeIndices[winnerIndex], _payout, _feePerPrizeClaim, _feeRecipient, _vaultPortion, _tierOdds, _drawDuration); - } - - return _prizeClaimCount; - } - - function _claimWinnerPrizes( - address _vault, - uint8 _tier, - address _winner, - uint32[] calldata _prizeIndices, - uint96 _payout, - uint96 _feePerPrizeClaim, - address _feeRecipient, - SD59x18 _vaultPortion, - SD59x18 _tierOdds, - uint16 _drawDuration - ) internal returns (uint32) { - for (uint256 _prizeArrayIndex = 0; _prizeArrayIndex < _prizeIndices.length; _prizeArrayIndex++) { - if (!_isWinner(_vault, _winner, _tier, _prizeIndices[_prizeArrayIndex], _vaultPortion, _tierOdds, _drawDuration)) { - revert DidNotWin(_winner, _vault, _tier, _prizeIndices[_prizeArrayIndex]); - } - - if (claimedPrizes[_winner][lastCompletedDrawId][_tier][_prizeIndices[_prizeArrayIndex]]) { - revert AlreadyClaimedPrize(_winner, _tier); - } - - claimedPrizes[_winner][lastCompletedDrawId][_tier][_prizeIndices[_prizeArrayIndex]] = true; - _transfer(_winner, _payout); - - emit ClaimedPrize(lastCompletedDrawId, _vault, _winner, _tier, uint152(_payout), _feePerPrizeClaim, _feeRecipient); - } - - return uint32(_prizeIndices.length); - } - - function _transfer(address _to, uint256 _amount) internal { - _totalWithdrawn += _amount; - prizeToken.safeTransfer(_to, _amount); - } - - /** - * @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 - */ - function withdrawClaimRewards(address _to, uint256 _amount) external { - uint256 _available = claimerRewards[msg.sender]; - - if (_amount > _available) { - revert InsufficientRewardsError(_amount, _available); - } - - claimerRewards[msg.sender] -= _amount; - _transfer(_to, _amount); - emit WithdrawClaimRewards(_to, _amount, _available); - } + function claimPrizes( + uint8 _tier, + address[] calldata _winners, + uint32[][] calldata _prizeIndices, + uint96 _feePerPrizeClaim, + address _feeRecipient + ) external returns (uint256) { + if (_winners.length != _prizeIndices.length) { + revert WinnerPrizeMismatch(uint128(_winners.length), uint128(_prizeIndices.length)); + } + + Tier memory tierLiquidity = _getTier(_tier, numberOfTiers); + if (_feePerPrizeClaim > tierLiquidity.prizeSize) { + revert FeeTooLarge(_feePerPrizeClaim, tierLiquidity.prizeSize); + } + + uint32 prizeClaimCount = _claimPrizes( + msg.sender, + _tier, + _winners, + _prizeIndices, + tierLiquidity.prizeSize - _feePerPrizeClaim, + _feePerPrizeClaim, + _feeRecipient + ); - /** - * @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 - */ - function balanceOfClaimRewards(address _claimer) external view returns (uint256) { - return claimerRewards[_claimer]; + if (_isCanaryTier(_tier, numberOfTiers)) { + canaryClaimCount += prizeClaimCount; + } else { + claimCount += prizeClaimCount; } - /** - * @notice Checks if the given user has won the prize for the specified tier in the given vault. - * @param _vault The address of the vault to check. - * @param _user The address of the user to check for the prize. - * @param _tier The tier for which the prize is to be checked. - * @return A boolean value indicating whether the user has won the prize or not. - */ - function isWinner( - address _vault, - address _user, - uint8 _tier, - uint32 _prizeIndex - ) external view returns (bool) { - (SD59x18 vaultPortion, SD59x18 tierOdds, uint16 drawDuration) = _computeVaultTierDetails(_vault, _tier, numberOfTiers, lastCompletedDrawId); - return _isWinner(_vault, _user, _tier, _prizeIndex, vaultPortion, tierOdds, drawDuration); + if (largestTierClaimed < _tier) { + largestTierClaimed = _tier; } - /** - * @notice Checks if the given user has won the prize for the specified tier in the given vault. - * @param _vault The address of the vault to check. - * @param _user The address of the user to check for the prize. - * @param _tier The tier for which the prize is to be checked. - * @return A boolean value indicating whether the user has won the prize or not. - */ - function _isWinner( - address _vault, - address _user, - uint8 _tier, - uint32 _prizeIndex, - SD59x18 _vaultPortion, - SD59x18 _tierOdds, - uint16 _drawDuration - ) internal view returns (bool) { - uint8 _numberOfTiers = numberOfTiers; - uint32 tierPrizeCount = _getTierPrizeCount(_tier, _numberOfTiers); - - if (_prizeIndex >= tierPrizeCount) { - revert InvalidPrizeIndex(_prizeIndex, tierPrizeCount, _tier); - } - - uint256 userSpecificRandomNumber = TierCalculationLib.calculatePseudoRandomNumber(_user, _tier, _prizeIndex, _winningRandomNumber); - (uint256 _userTwab, uint256 _vaultTwabTotalSupply) = _getVaultUserBalanceAndTotalSupplyTwab(_vault, _user, _drawDuration); - - return TierCalculationLib.isWinner(userSpecificRandomNumber, uint128(_userTwab), uint128(_vaultTwabTotalSupply), _vaultPortion, _tierOdds); - } + _consumeLiquidity(tierLiquidity, _tier, tierLiquidity.prizeSize * prizeClaimCount); - function _computeVaultTierDetails(address _vault, uint8 _tier, uint8 _numberOfTiers, uint16 _lastCompletedDrawId) internal view returns (SD59x18 vaultPortion, SD59x18 tierOdds, uint16 drawDuration) { - if (_lastCompletedDrawId == 0) { - revert NoCompletedDraw(); - } - if (_tier > _numberOfTiers) { - revert InvalidTier(_tier, _numberOfTiers); - } - - tierOdds = TierCalculationLib.getTierOdds(_tier, _numberOfTiers, grandPrizePeriodDraws); - drawDuration = uint16(TierCalculationLib.estimatePrizeFrequencyInDraws(_tier, _numberOfTiers, grandPrizePeriodDraws)); - vaultPortion = _getVaultPortion( - _vault, - uint16(drawDuration > _lastCompletedDrawId ? 0 : _lastCompletedDrawId - drawDuration + 1), - _lastCompletedDrawId + 1, - smoothing.intoSD59x18() - ); + if (_feePerPrizeClaim != 0 && prizeClaimCount != 0) { + claimerRewards[_feeRecipient] += _feePerPrizeClaim * prizeClaimCount; } - /*** - * @notice Calculates the start and end timestamps of the time-weighted average balance (TWAB) for the specified tier. - * @param _tier The tier for which to calculate the TWAB timestamps. - * @return The start and end timestamps of the TWAB. - */ - function calculateTierTwabTimestamps(uint8 _tier) external view returns (uint64 startTimestamp, uint64 endTimestamp) { - endTimestamp = _lastCompletedDrawStartedAt + drawPeriodSeconds; - - // endTimestamp - (drawDuration * drawPeriodSeconds) - startTimestamp = uint64(endTimestamp - TierCalculationLib.estimatePrizeFrequencyInDraws(_tier, numberOfTiers, grandPrizePeriodDraws) * drawPeriodSeconds); - } + return tierLiquidity.prizeSize * prizeClaimCount; + } - /** - * @notice Returns the time-weighted average balance (TWAB) and the TWAB total supply for the specified user in the given vault over a specified period. - * @dev This function calculates the TWAB for a user by calling the getTwabBetween function of the TWAB controller for a specified period of time. - * @param _vault The address of the vault for which to get the TWAB. - * @param _user The address of the user for which to get the TWAB. - * @param _drawDuration The duration of the period over which to calculate the TWAB, in number of draw periods. - * @return twab The TWAB for the specified user in the given vault over the specified period. - * @return twabTotalSupply The TWAB total supply over the specified period. - */ - function _getVaultUserBalanceAndTotalSupplyTwab(address _vault, address _user, uint256 _drawDuration) internal view returns (uint256 twab, uint256 twabTotalSupply) { - uint32 _endTimestamp = uint32(_lastCompletedDrawStartedAt + drawPeriodSeconds); - uint32 _startTimestamp = uint32(_endTimestamp - _drawDuration * drawPeriodSeconds); - - twab = twabController.getTwabBetween( - _vault, - _user, - _startTimestamp, - _endTimestamp - ); - - twabTotalSupply = twabController.getTotalSupplyTwabBetween( - _vault, - _startTimestamp, - _endTimestamp - ); - } + function _claimPrizes( + address _vault, + uint8 _tier, + address[] calldata _winners, + uint32[][] calldata _prizeIndices, + uint96 _payout, + uint96 _feePerPrizeClaim, + address _feeRecipient + ) internal returns (uint32) { + uint32 _prizeClaimCount = 0; + (SD59x18 _vaultPortion, SD59x18 _tierOdds, uint16 _drawDuration) = _computeVaultTierDetails( + _vault, + _tier, + numberOfTiers, + lastCompletedDrawId + ); - /** - * @notice Returns the time-weighted average balance (TWAB) and the TWAB total supply for the specified user in the given vault over a specified period. - * @param _vault The address of the vault for which to get the TWAB. - * @param _user The address of the user for which to get the TWAB. - * @param _drawDuration The duration of the period over which to calculate the TWAB, in number of draw periods. - * @return The TWAB and the TWAB total supply for the specified user in the given vault over the specified period. - */ - function getVaultUserBalanceAndTotalSupplyTwab(address _vault, address _user, uint256 _drawDuration) external view returns (uint256, uint256) { - return _getVaultUserBalanceAndTotalSupplyTwab(_vault, _user, _drawDuration); - } + for (uint winnerIndex = 0; winnerIndex < _winners.length; winnerIndex++) { + _prizeClaimCount += _claimWinnerPrizes( + _vault, + _tier, + _winners[winnerIndex], + _prizeIndices[winnerIndex], + _payout, + _feePerPrizeClaim, + _feeRecipient, + _vaultPortion, + _tierOdds, + _drawDuration + ); + } + + return _prizeClaimCount; + } + + function _claimWinnerPrizes( + address _vault, + uint8 _tier, + address _winner, + uint32[] calldata _prizeIndices, + uint96 _payout, + uint96 _feePerPrizeClaim, + address _feeRecipient, + SD59x18 _vaultPortion, + SD59x18 _tierOdds, + uint16 _drawDuration + ) internal returns (uint32) { + for ( + uint256 _prizeArrayIndex = 0; + _prizeArrayIndex < _prizeIndices.length; + _prizeArrayIndex++ + ) { + if ( + !_isWinner( + _vault, + _winner, + _tier, + _prizeIndices[_prizeArrayIndex], + _vaultPortion, + _tierOdds, + _drawDuration + ) + ) { + revert DidNotWin(_winner, _vault, _tier, _prizeIndices[_prizeArrayIndex]); + } + + if (claimedPrizes[_winner][lastCompletedDrawId][_tier][_prizeIndices[_prizeArrayIndex]]) { + revert AlreadyClaimedPrize(_winner, _tier); + } + + claimedPrizes[_winner][lastCompletedDrawId][_tier][_prizeIndices[_prizeArrayIndex]] = true; + _transfer(_winner, _payout); + + emit ClaimedPrize( + lastCompletedDrawId, + _vault, + _winner, + _tier, + uint152(_payout), + _feePerPrizeClaim, + _feeRecipient + ); + } + + return uint32(_prizeIndices.length); + } + + function _transfer(address _to, uint256 _amount) internal { + _totalWithdrawn += _amount; + prizeToken.safeTransfer(_to, _amount); + } + + /** + * @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 + */ + function withdrawClaimRewards(address _to, uint256 _amount) external { + uint256 _available = claimerRewards[msg.sender]; + + if (_amount > _available) { + revert InsufficientRewardsError(_amount, _available); + } + + claimerRewards[msg.sender] -= _amount; + _transfer(_to, _amount); + emit WithdrawClaimRewards(_to, _amount, _available); + } + + /** + * @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 + */ + function balanceOfClaimRewards(address _claimer) external view returns (uint256) { + return claimerRewards[_claimer]; + } + + /** + * @notice Checks if the given user has won the prize for the specified tier in the given vault. + * @param _vault The address of the vault to check. + * @param _user The address of the user to check for the prize. + * @param _tier The tier for which the prize is to be checked. + * @return A boolean value indicating whether the user has won the prize or not. + */ + function isWinner( + address _vault, + address _user, + uint8 _tier, + uint32 _prizeIndex + ) external view returns (bool) { + (SD59x18 vaultPortion, SD59x18 tierOdds, uint16 drawDuration) = _computeVaultTierDetails( + _vault, + _tier, + numberOfTiers, + lastCompletedDrawId + ); + return _isWinner(_vault, _user, _tier, _prizeIndex, vaultPortion, tierOdds, drawDuration); + } + + /** + * @notice Checks if the given user has won the prize for the specified tier in the given vault. + * @param _vault The address of the vault to check. + * @param _user The address of the user to check for the prize. + * @param _tier The tier for which the prize is to be checked. + * @return A boolean value indicating whether the user has won the prize or not. + */ + function _isWinner( + address _vault, + address _user, + uint8 _tier, + uint32 _prizeIndex, + SD59x18 _vaultPortion, + SD59x18 _tierOdds, + uint16 _drawDuration + ) internal view returns (bool) { + uint8 _numberOfTiers = numberOfTiers; + uint32 tierPrizeCount = _getTierPrizeCount(_tier, _numberOfTiers); + + if (_prizeIndex >= tierPrizeCount) { + revert InvalidPrizeIndex(_prizeIndex, tierPrizeCount, _tier); + } + + uint256 userSpecificRandomNumber = TierCalculationLib.calculatePseudoRandomNumber( + _user, + _tier, + _prizeIndex, + _winningRandomNumber + ); + (uint256 _userTwab, uint256 _vaultTwabTotalSupply) = _getVaultUserBalanceAndTotalSupplyTwab( + _vault, + _user, + _drawDuration + ); - /** - * @notice Calculates the portion of the vault's contribution to the prize pool over a specified duration in draws. - * @param _vault The address of the vault for which to calculate the portion. - * @param _startDrawId The starting draw ID (inclusive) of the draw range to calculate the contribution portion for. - * @param _endDrawId The ending draw ID (inclusive) of the draw range to calculate the contribution portion for. - * @param _smoothing The smoothing value to use for calculating the portion. - * @return The portion of the vault's contribution to the prize pool over the specified duration in draws. - */ - function _getVaultPortion(address _vault, uint16 _startDrawId, uint16 _endDrawId, SD59x18 _smoothing) internal view returns (SD59x18) { - uint256 totalContributed = DrawAccumulatorLib.getDisbursedBetween(totalAccumulator, _startDrawId, _endDrawId, _smoothing); - - if (totalContributed != 0) { - // vaultContributed / totalContributed - return sd(int256( - DrawAccumulatorLib.getDisbursedBetween(vaultAccumulator[_vault], _startDrawId, _endDrawId, _smoothing) - )).div(sd(int256(totalContributed))); - } else { - return sd(0); - } - } + return + TierCalculationLib.isWinner( + userSpecificRandomNumber, + uint128(_userTwab), + uint128(_vaultTwabTotalSupply), + _vaultPortion, + _tierOdds + ); + } + + function _computeVaultTierDetails( + address _vault, + uint8 _tier, + uint8 _numberOfTiers, + uint16 _lastCompletedDrawId + ) internal view returns (SD59x18 vaultPortion, SD59x18 tierOdds, uint16 drawDuration) { + if (_lastCompletedDrawId == 0) { + revert NoCompletedDraw(); + } + if (_tier > _numberOfTiers) { + revert InvalidTier(_tier, _numberOfTiers); + } + + tierOdds = TierCalculationLib.getTierOdds(_tier, _numberOfTiers, grandPrizePeriodDraws); + drawDuration = uint16( + TierCalculationLib.estimatePrizeFrequencyInDraws(_tier, _numberOfTiers, grandPrizePeriodDraws) + ); + vaultPortion = _getVaultPortion( + _vault, + uint16(drawDuration > _lastCompletedDrawId ? 0 : _lastCompletedDrawId - drawDuration + 1), + _lastCompletedDrawId + 1, + smoothing.intoSD59x18() + ); + } + + /*** + * @notice Calculates the start and end timestamps of the time-weighted average balance (TWAB) for the specified tier. + * @param _tier The tier for which to calculate the TWAB timestamps. + * @return The start and end timestamps of the TWAB. + */ + function calculateTierTwabTimestamps( + uint8 _tier + ) external view returns (uint64 startTimestamp, uint64 endTimestamp) { + endTimestamp = _lastCompletedDrawStartedAt + drawPeriodSeconds; + + // endTimestamp - (drawDuration * drawPeriodSeconds) + startTimestamp = uint64( + endTimestamp - + TierCalculationLib.estimatePrizeFrequencyInDraws( + _tier, + numberOfTiers, + grandPrizePeriodDraws + ) * + drawPeriodSeconds + ); + } + + /** + * @notice Returns the time-weighted average balance (TWAB) and the TWAB total supply for the specified user in the given vault over a specified period. + * @dev This function calculates the TWAB for a user by calling the getTwabBetween function of the TWAB controller for a specified period of time. + * @param _vault The address of the vault for which to get the TWAB. + * @param _user The address of the user for which to get the TWAB. + * @param _drawDuration The duration of the period over which to calculate the TWAB, in number of draw periods. + * @return twab The TWAB for the specified user in the given vault over the specified period. + * @return twabTotalSupply The TWAB total supply over the specified period. + */ + function _getVaultUserBalanceAndTotalSupplyTwab( + address _vault, + address _user, + uint256 _drawDuration + ) internal view returns (uint256 twab, uint256 twabTotalSupply) { + uint32 _endTimestamp = uint32(_lastCompletedDrawStartedAt + drawPeriodSeconds); + uint32 _startTimestamp = uint32(_endTimestamp - _drawDuration * drawPeriodSeconds); + + twab = twabController.getTwabBetween(_vault, _user, _startTimestamp, _endTimestamp); + + twabTotalSupply = twabController.getTotalSupplyTwabBetween( + _vault, + _startTimestamp, + _endTimestamp + ); + } + + /** + * @notice Returns the time-weighted average balance (TWAB) and the TWAB total supply for the specified user in the given vault over a specified period. + * @param _vault The address of the vault for which to get the TWAB. + * @param _user The address of the user for which to get the TWAB. + * @param _drawDuration The duration of the period over which to calculate the TWAB, in number of draw periods. + * @return The TWAB and the TWAB total supply for the specified user in the given vault over the specified period. + */ + function getVaultUserBalanceAndTotalSupplyTwab( + address _vault, + address _user, + uint256 _drawDuration + ) external view returns (uint256, uint256) { + return _getVaultUserBalanceAndTotalSupplyTwab(_vault, _user, _drawDuration); + } + + /** + * @notice Calculates the portion of the vault's contribution to the prize pool over a specified duration in draws. + * @param _vault The address of the vault for which to calculate the portion. + * @param _startDrawId The starting draw ID (inclusive) of the draw range to calculate the contribution portion for. + * @param _endDrawId The ending draw ID (inclusive) of the draw range to calculate the contribution portion for. + * @param _smoothing The smoothing value to use for calculating the portion. + * @return The portion of the vault's contribution to the prize pool over the specified duration in draws. + */ + function _getVaultPortion( + address _vault, + uint16 _startDrawId, + uint16 _endDrawId, + SD59x18 _smoothing + ) internal view returns (SD59x18) { + uint256 totalContributed = DrawAccumulatorLib.getDisbursedBetween( + totalAccumulator, + _startDrawId, + _endDrawId, + _smoothing + ); - /** + if (totalContributed != 0) { + // vaultContributed / totalContributed + return + sd( + int256( + DrawAccumulatorLib.getDisbursedBetween( + vaultAccumulator[_vault], + _startDrawId, + _endDrawId, + _smoothing + ) + ) + ).div(sd(int256(totalContributed))); + } else { + return sd(0); + } + } + + /** @notice Returns the portion of a vault's contributions in a given draw range. This function takes in an address _vault, a uint16 startDrawId, and a uint16 endDrawId. It calculates the portion of the _vault's contributions in the given draw range by calling the internal @@ -781,7 +945,11 @@ contract PrizePool is TieredLiquidityDistributor { @param _endDrawId The ending draw ID of the draw range to calculate the contribution portion for. @return The portion of the _vault's contributions in the given draw range as an SD59x18 value. */ - function getVaultPortion(address _vault, uint16 _startDrawId, uint16 _endDrawId) external view returns (SD59x18) { - return _getVaultPortion(_vault, _startDrawId, _endDrawId, smoothing.intoSD59x18()); - } + function getVaultPortion( + address _vault, + uint16 _startDrawId, + uint16 _endDrawId + ) external view returns (SD59x18) { + return _getVaultPortion(_vault, _startDrawId, _endDrawId, smoothing.intoSD59x18()); + } } diff --git a/src/abstract/TieredLiquidityDistributor.sol b/src/abstract/TieredLiquidityDistributor.sol index c6919a9..6b9227a 100644 --- a/src/abstract/TieredLiquidityDistributor.sol +++ b/src/abstract/TieredLiquidityDistributor.sol @@ -12,9 +12,9 @@ import { TierCalculationLib } from "../libraries/TierCalculationLib.sol"; /// @notice Struct that tracks tier liquidity information struct Tier { - uint16 drawId; - uint96 prizeSize; - UD34x4 prizeTokenPerShare; + uint16 drawId; + uint96 prizeSize; + UD34x4 prizeTokenPerShare; } /// @notice Emitted when the number of tiers is less than the minimum number of tiers @@ -29,494 +29,702 @@ error InsufficientLiquidity(uint104 requestedLiquidity); /// @author PoolTogether Inc. /// @notice A contract that distributes liquidity according to PoolTogether V5 distribution rules. contract TieredLiquidityDistributor { - - uint8 internal constant MINIMUM_NUMBER_OF_TIERS = 3; - - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS; - uint32 immutable internal ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS; - - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_2_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_3_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_4_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_5_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_6_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_7_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_8_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_9_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_10_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_11_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_12_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_13_TIERS; - UD60x18 immutable internal CANARY_PRIZE_COUNT_FOR_14_TIERS; - - /// @notice The Tier liquidity data - mapping(uint8 => Tier) internal _tiers; - - /// @notice The number of draws that should statistically occur between grand prizes. - uint16 public immutable grandPrizePeriodDraws; - - /// @notice The number of shares to allocate to each prize tier - uint8 public immutable tierShares; - - /// @notice The number of shares to allocate to the canary tier - uint8 public immutable canaryShares; - - /// @notice The number of shares to allocate to the reserve - uint8 public immutable reserveShares; - - /// @notice The current number of prize tokens per share - UD34x4 public prizeTokenPerShare; - - /// @notice The number of tiers for the last completed draw. The last tier is the canary tier. - uint8 public numberOfTiers; - - /// @notice The draw id of the last completed draw - uint16 internal lastCompletedDrawId; - - /// @notice The amount of available reserve - uint104 internal _reserve; - - /** - * @notice Constructs a new Prize Pool - * @param _grandPrizePeriodDraws The average number of draws between grand prizes. This determines the statistical frequency of grand prizes. - * @param _numberOfTiers The number of tiers to start with. Must be greater than or equal to the minimum number of tiers. - * @param _tierShares The number of shares to allocate to each tier - * @param _canaryShares The number of shares to allocate to the canary tier. - * @param _reserveShares The number of shares to allocate to the reserve. - */ - constructor ( - uint16 _grandPrizePeriodDraws, - uint8 _numberOfTiers, - uint8 _tierShares, - uint8 _canaryShares, - uint8 _reserveShares - ) { - grandPrizePeriodDraws = _grandPrizePeriodDraws; - numberOfTiers = _numberOfTiers; - tierShares = _tierShares; - canaryShares = _canaryShares; - reserveShares = _reserveShares; - - ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS = TierCalculationLib.estimatedClaimCount(2, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS = TierCalculationLib.estimatedClaimCount(3, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS = TierCalculationLib.estimatedClaimCount(4, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS = TierCalculationLib.estimatedClaimCount(5, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS = TierCalculationLib.estimatedClaimCount(6, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS = TierCalculationLib.estimatedClaimCount(7, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS = TierCalculationLib.estimatedClaimCount(8, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS = TierCalculationLib.estimatedClaimCount(9, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS = TierCalculationLib.estimatedClaimCount(10, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS = TierCalculationLib.estimatedClaimCount(11, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS = TierCalculationLib.estimatedClaimCount(12, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS = TierCalculationLib.estimatedClaimCount(13, _grandPrizePeriodDraws); - ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS = TierCalculationLib.estimatedClaimCount(14, _grandPrizePeriodDraws); - - CANARY_PRIZE_COUNT_FOR_2_TIERS = TierCalculationLib.canaryPrizeCount(2, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_3_TIERS = TierCalculationLib.canaryPrizeCount(3, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_4_TIERS = TierCalculationLib.canaryPrizeCount(4, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_5_TIERS = TierCalculationLib.canaryPrizeCount(5, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_6_TIERS = TierCalculationLib.canaryPrizeCount(6, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_7_TIERS = TierCalculationLib.canaryPrizeCount(7, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_8_TIERS = TierCalculationLib.canaryPrizeCount(8, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_9_TIERS = TierCalculationLib.canaryPrizeCount(9, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_10_TIERS = TierCalculationLib.canaryPrizeCount(10, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_11_TIERS = TierCalculationLib.canaryPrizeCount(11, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_12_TIERS = TierCalculationLib.canaryPrizeCount(12, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_13_TIERS = TierCalculationLib.canaryPrizeCount(13, _canaryShares, _reserveShares, _tierShares); - CANARY_PRIZE_COUNT_FOR_14_TIERS = TierCalculationLib.canaryPrizeCount(14, _canaryShares, _reserveShares, _tierShares); - - if(_numberOfTiers < MINIMUM_NUMBER_OF_TIERS) { - revert NumberOfTiersLessThanMinimum(_numberOfTiers); - } - } - - /// @notice Adjusts the number of tiers and distributes new liquidity - /// @param _nextNumberOfTiers The new number of tiers. Must be greater than minimum - /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve - function _nextDraw(uint8 _nextNumberOfTiers, uint96 _prizeTokenLiquidity) internal { - if(_nextNumberOfTiers < MINIMUM_NUMBER_OF_TIERS) { - revert NumberOfTiersLessThanMinimum(_nextNumberOfTiers); - } - - uint8 numTiers = numberOfTiers; - UD60x18 _prizeTokenPerShare = fromUD34x4toUD60x18(prizeTokenPerShare); - (uint16 completedDrawId, uint104 newReserve, UD60x18 newPrizeTokenPerShare) = _computeNewDistributions(numTiers, _nextNumberOfTiers, _prizeTokenPerShare, _prizeTokenLiquidity); - - // need to redistribute to the canary tier and any new tiers (if expanding) - uint8 start; - uint8 end; - // if we are expanding, need to reset the canary tier and all of the new tiers - if (_nextNumberOfTiers > numTiers) { - start = numTiers - 1; - end = _nextNumberOfTiers; - } else { // just reset the canary tier - start = _nextNumberOfTiers - 1; - end = _nextNumberOfTiers; - } - for (uint8 i = start; i < end; i++) { - _tiers[i] = Tier({ - drawId: completedDrawId, - prizeTokenPerShare: prizeTokenPerShare, - prizeSize: uint96(_computePrizeSize(i, _nextNumberOfTiers, _prizeTokenPerShare, newPrizeTokenPerShare)) - }); - } - - prizeTokenPerShare = fromUD60x18toUD34x4(newPrizeTokenPerShare); - numberOfTiers = _nextNumberOfTiers; - lastCompletedDrawId = completedDrawId; - _reserve += newReserve; - - } - - /// @notice Computes the liquidity that will be distributed for the next draw given the next number of tiers and prize liquidity. - /// @param _numberOfTiers The current number of tiers - /// @param _nextNumberOfTiers The next number of tiers to use to compute distribution - /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve - /// @return completedDrawId The drawId that this is for - /// @return newReserve The amount of liquidity that will be added to the reserve - /// @return newPrizeTokenPerShare The new prize token per share - function _computeNewDistributions(uint8 _numberOfTiers, uint8 _nextNumberOfTiers, uint256 _prizeTokenLiquidity) internal view returns (uint16 completedDrawId, uint104 newReserve, UD60x18 newPrizeTokenPerShare) { - return _computeNewDistributions(_numberOfTiers, _nextNumberOfTiers, fromUD34x4toUD60x18(prizeTokenPerShare), _prizeTokenLiquidity); - } - - /// @notice Computes the liquidity that will be distributed for the next draw given the next number of tiers and prize liquidity. - /// @param _numberOfTiers The current number of tiers - /// @param _nextNumberOfTiers The next number of tiers to use to compute distribution - /// @param _currentPrizeTokenPerShare The current prize token per share - /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve - /// @return completedDrawId The drawId that this is for - /// @return newReserve The amount of liquidity that will be added to the reserve - /// @return newPrizeTokenPerShare The new prize token per share - function _computeNewDistributions(uint8 _numberOfTiers, uint8 _nextNumberOfTiers, UD60x18 _currentPrizeTokenPerShare, uint _prizeTokenLiquidity) internal view returns (uint16 completedDrawId, uint104 newReserve, UD60x18 newPrizeTokenPerShare) { - completedDrawId = lastCompletedDrawId + 1; - uint256 totalShares = _getTotalShares(_nextNumberOfTiers); - UD60x18 deltaPrizeTokensPerShare = (toUD60x18(_prizeTokenLiquidity).div(toUD60x18(totalShares))).floor(); - - newPrizeTokenPerShare = _currentPrizeTokenPerShare.add(deltaPrizeTokensPerShare); - - uint reclaimed = _getTierLiquidityToReclaim(_numberOfTiers, _nextNumberOfTiers, _currentPrizeTokenPerShare); - uint computedLiquidity = fromUD60x18(deltaPrizeTokensPerShare.mul(toUD60x18(totalShares))); - uint remainder = (_prizeTokenLiquidity - computedLiquidity); - - - newReserve = uint104( - fromUD60x18(deltaPrizeTokensPerShare.mul(toUD60x18(reserveShares))) + // reserve portion - reclaimed + // reclaimed liquidity from tiers - remainder // remainder - ); - - } - - /// @notice Returns the prize size for the given tier - /// @param _tier The tier to retrieve - /// @return The prize size for the tier - function getTierPrizeSize(uint8 _tier) external view returns (uint96) { - return _getTier(_tier, numberOfTiers).prizeSize; - } - - /// @notice Returns the estimated number of prizes for the given tier - /// @return The estimated number of prizes - function getTierPrizeCount(uint8 _tier) external view returns (uint32) { - return _getTierPrizeCount(_tier, numberOfTiers); + uint8 internal constant MINIMUM_NUMBER_OF_TIERS = 3; + + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS; + uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS; + + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_2_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_3_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_4_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_5_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_6_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_7_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_8_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_9_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_10_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_11_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_12_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_13_TIERS; + UD60x18 internal immutable CANARY_PRIZE_COUNT_FOR_14_TIERS; + + /// @notice The Tier liquidity data + mapping(uint8 => Tier) internal _tiers; + + /// @notice The number of draws that should statistically occur between grand prizes. + uint16 public immutable grandPrizePeriodDraws; + + /// @notice The number of shares to allocate to each prize tier + uint8 public immutable tierShares; + + /// @notice The number of shares to allocate to the canary tier + uint8 public immutable canaryShares; + + /// @notice The number of shares to allocate to the reserve + uint8 public immutable reserveShares; + + /// @notice The current number of prize tokens per share + UD34x4 public prizeTokenPerShare; + + /// @notice The number of tiers for the last completed draw. The last tier is the canary tier. + uint8 public numberOfTiers; + + /// @notice The draw id of the last completed draw + uint16 internal lastCompletedDrawId; + + /// @notice The amount of available reserve + uint104 internal _reserve; + + /** + * @notice Constructs a new Prize Pool + * @param _grandPrizePeriodDraws The average number of draws between grand prizes. This determines the statistical frequency of grand prizes. + * @param _numberOfTiers The number of tiers to start with. Must be greater than or equal to the minimum number of tiers. + * @param _tierShares The number of shares to allocate to each tier + * @param _canaryShares The number of shares to allocate to the canary tier. + * @param _reserveShares The number of shares to allocate to the reserve. + */ + constructor( + uint16 _grandPrizePeriodDraws, + uint8 _numberOfTiers, + uint8 _tierShares, + uint8 _canaryShares, + uint8 _reserveShares + ) { + grandPrizePeriodDraws = _grandPrizePeriodDraws; + numberOfTiers = _numberOfTiers; + tierShares = _tierShares; + canaryShares = _canaryShares; + reserveShares = _reserveShares; + + ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS = TierCalculationLib.estimatedClaimCount( + 2, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS = TierCalculationLib.estimatedClaimCount( + 3, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS = TierCalculationLib.estimatedClaimCount( + 4, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS = TierCalculationLib.estimatedClaimCount( + 5, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS = TierCalculationLib.estimatedClaimCount( + 6, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS = TierCalculationLib.estimatedClaimCount( + 7, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS = TierCalculationLib.estimatedClaimCount( + 8, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS = TierCalculationLib.estimatedClaimCount( + 9, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS = TierCalculationLib.estimatedClaimCount( + 10, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS = TierCalculationLib.estimatedClaimCount( + 11, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS = TierCalculationLib.estimatedClaimCount( + 12, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS = TierCalculationLib.estimatedClaimCount( + 13, + _grandPrizePeriodDraws + ); + ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS = TierCalculationLib.estimatedClaimCount( + 14, + _grandPrizePeriodDraws + ); + + CANARY_PRIZE_COUNT_FOR_2_TIERS = TierCalculationLib.canaryPrizeCount( + 2, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_3_TIERS = TierCalculationLib.canaryPrizeCount( + 3, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_4_TIERS = TierCalculationLib.canaryPrizeCount( + 4, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_5_TIERS = TierCalculationLib.canaryPrizeCount( + 5, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_6_TIERS = TierCalculationLib.canaryPrizeCount( + 6, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_7_TIERS = TierCalculationLib.canaryPrizeCount( + 7, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_8_TIERS = TierCalculationLib.canaryPrizeCount( + 8, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_9_TIERS = TierCalculationLib.canaryPrizeCount( + 9, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_10_TIERS = TierCalculationLib.canaryPrizeCount( + 10, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_11_TIERS = TierCalculationLib.canaryPrizeCount( + 11, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_12_TIERS = TierCalculationLib.canaryPrizeCount( + 12, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_13_TIERS = TierCalculationLib.canaryPrizeCount( + 13, + _canaryShares, + _reserveShares, + _tierShares + ); + CANARY_PRIZE_COUNT_FOR_14_TIERS = TierCalculationLib.canaryPrizeCount( + 14, + _canaryShares, + _reserveShares, + _tierShares + ); + + if (_numberOfTiers < MINIMUM_NUMBER_OF_TIERS) { + revert NumberOfTiersLessThanMinimum(_numberOfTiers); } - - /// @notice Returns the number of available prizes for the given tier - /// @param _tier The tier to retrieve - /// @param _numberOfTiers The number of tiers, should match the current number of tiers - /// @return The number of available prizes - function _getTierPrizeCount(uint8 _tier, uint8 _numberOfTiers) internal view returns (uint32) { - return _isCanaryTier(_tier, _numberOfTiers) ? _canaryPrizeCount(_numberOfTiers) : uint32(TierCalculationLib.prizeCount(_tier)); + } + + /// @notice Adjusts the number of tiers and distributes new liquidity + /// @param _nextNumberOfTiers The new number of tiers. Must be greater than minimum + /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve + function _nextDraw(uint8 _nextNumberOfTiers, uint96 _prizeTokenLiquidity) internal { + if (_nextNumberOfTiers < MINIMUM_NUMBER_OF_TIERS) { + revert NumberOfTiersLessThanMinimum(_nextNumberOfTiers); } - /// @notice Computes the remaining liquidity for the given tier - /// @param _tier The tier to compute the remaining liquidity for - /// @return The remaining liquidity - function getTierRemainingLiquidity(uint8 _tier) external view returns (uint112) { - uint8 numTiers = numberOfTiers; - return uint104(_remainingTierLiquidity(_getTier(_tier, numTiers), _computeShares(_tier, numTiers))); + uint8 numTiers = numberOfTiers; + UD60x18 _prizeTokenPerShare = fromUD34x4toUD60x18(prizeTokenPerShare); + ( + uint16 completedDrawId, + uint104 newReserve, + UD60x18 newPrizeTokenPerShare + ) = _computeNewDistributions( + numTiers, + _nextNumberOfTiers, + _prizeTokenPerShare, + _prizeTokenLiquidity + ); + + // need to redistribute to the canary tier and any new tiers (if expanding) + uint8 start; + uint8 end; + // if we are expanding, need to reset the canary tier and all of the new tiers + if (_nextNumberOfTiers > numTiers) { + start = numTiers - 1; + end = _nextNumberOfTiers; + } else { + // just reset the canary tier + start = _nextNumberOfTiers - 1; + end = _nextNumberOfTiers; } - - /// @notice Retrieves an up-to-date Tier struct for the given tier - /// @param _tier The tier to retrieve - /// @param _numberOfTiers The number of tiers, should match the current. Passed explicitly as an optimization - /// @return An up-to-date Tier struct; if the prize is outdated then it is recomputed based on available liquidity and the draw id updated. - function _getTier(uint8 _tier, uint8 _numberOfTiers) internal view returns (Tier memory) { - Tier memory tier = _tiers[_tier]; - uint16 _lastCompletedDrawId = lastCompletedDrawId; - if (tier.drawId != _lastCompletedDrawId) { - tier.drawId = _lastCompletedDrawId; - tier.prizeSize = uint96(_computePrizeSize(_tier, _numberOfTiers, fromUD34x4toUD60x18(tier.prizeTokenPerShare), fromUD34x4toUD60x18(prizeTokenPerShare))); - } - return tier; + for (uint8 i = start; i < end; i++) { + _tiers[i] = Tier({ + drawId: completedDrawId, + prizeTokenPerShare: prizeTokenPerShare, + prizeSize: uint96( + _computePrizeSize(i, _nextNumberOfTiers, _prizeTokenPerShare, newPrizeTokenPerShare) + ) + }); } - /// @notice Computes the total shares in the system. That is `(number of tiers * tier shares) + canary shares + reserve shares` - /// @return The total shares - function getTotalShares() external view returns (uint256) { - return _getTotalShares(numberOfTiers); + prizeTokenPerShare = fromUD60x18toUD34x4(newPrizeTokenPerShare); + numberOfTiers = _nextNumberOfTiers; + lastCompletedDrawId = completedDrawId; + _reserve += newReserve; + } + + /// @notice Computes the liquidity that will be distributed for the next draw given the next number of tiers and prize liquidity. + /// @param _numberOfTiers The current number of tiers + /// @param _nextNumberOfTiers The next number of tiers to use to compute distribution + /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve + /// @return completedDrawId The drawId that this is for + /// @return newReserve The amount of liquidity that will be added to the reserve + /// @return newPrizeTokenPerShare The new prize token per share + function _computeNewDistributions( + uint8 _numberOfTiers, + uint8 _nextNumberOfTiers, + uint256 _prizeTokenLiquidity + ) + internal + view + returns (uint16 completedDrawId, uint104 newReserve, UD60x18 newPrizeTokenPerShare) + { + return + _computeNewDistributions( + _numberOfTiers, + _nextNumberOfTiers, + fromUD34x4toUD60x18(prizeTokenPerShare), + _prizeTokenLiquidity + ); + } + + /// @notice Computes the liquidity that will be distributed for the next draw given the next number of tiers and prize liquidity. + /// @param _numberOfTiers The current number of tiers + /// @param _nextNumberOfTiers The next number of tiers to use to compute distribution + /// @param _currentPrizeTokenPerShare The current prize token per share + /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve + /// @return completedDrawId The drawId that this is for + /// @return newReserve The amount of liquidity that will be added to the reserve + /// @return newPrizeTokenPerShare The new prize token per share + function _computeNewDistributions( + uint8 _numberOfTiers, + uint8 _nextNumberOfTiers, + UD60x18 _currentPrizeTokenPerShare, + uint _prizeTokenLiquidity + ) + internal + view + returns (uint16 completedDrawId, uint104 newReserve, UD60x18 newPrizeTokenPerShare) + { + completedDrawId = lastCompletedDrawId + 1; + uint256 totalShares = _getTotalShares(_nextNumberOfTiers); + UD60x18 deltaPrizeTokensPerShare = (toUD60x18(_prizeTokenLiquidity).div(toUD60x18(totalShares))) + .floor(); + + newPrizeTokenPerShare = _currentPrizeTokenPerShare.add(deltaPrizeTokensPerShare); + + uint reclaimed = _getTierLiquidityToReclaim( + _numberOfTiers, + _nextNumberOfTiers, + _currentPrizeTokenPerShare + ); + uint computedLiquidity = fromUD60x18(deltaPrizeTokensPerShare.mul(toUD60x18(totalShares))); + uint remainder = (_prizeTokenLiquidity - computedLiquidity); + + newReserve = uint104( + fromUD60x18(deltaPrizeTokensPerShare.mul(toUD60x18(reserveShares))) + // reserve portion + reclaimed + // reclaimed liquidity from tiers + remainder // remainder + ); + } + + /// @notice Returns the prize size for the given tier + /// @param _tier The tier to retrieve + /// @return The prize size for the tier + function getTierPrizeSize(uint8 _tier) external view returns (uint96) { + return _getTier(_tier, numberOfTiers).prizeSize; + } + + /// @notice Returns the estimated number of prizes for the given tier + /// @return The estimated number of prizes + function getTierPrizeCount(uint8 _tier) external view returns (uint32) { + return _getTierPrizeCount(_tier, numberOfTiers); + } + + /// @notice Returns the number of available prizes for the given tier + /// @param _tier The tier to retrieve + /// @param _numberOfTiers The number of tiers, should match the current number of tiers + /// @return The number of available prizes + function _getTierPrizeCount(uint8 _tier, uint8 _numberOfTiers) internal view returns (uint32) { + return + _isCanaryTier(_tier, _numberOfTiers) + ? _canaryPrizeCount(_numberOfTiers) + : uint32(TierCalculationLib.prizeCount(_tier)); + } + + /// @notice Computes the remaining liquidity for the given tier + /// @param _tier The tier to compute the remaining liquidity for + /// @return The remaining liquidity + function getTierRemainingLiquidity(uint8 _tier) external view returns (uint112) { + uint8 numTiers = numberOfTiers; + return + uint104(_remainingTierLiquidity(_getTier(_tier, numTiers), _computeShares(_tier, numTiers))); + } + + /// @notice Retrieves an up-to-date Tier struct for the given tier + /// @param _tier The tier to retrieve + /// @param _numberOfTiers The number of tiers, should match the current. Passed explicitly as an optimization + /// @return An up-to-date Tier struct; if the prize is outdated then it is recomputed based on available liquidity and the draw id updated. + function _getTier(uint8 _tier, uint8 _numberOfTiers) internal view returns (Tier memory) { + Tier memory tier = _tiers[_tier]; + uint16 _lastCompletedDrawId = lastCompletedDrawId; + if (tier.drawId != _lastCompletedDrawId) { + tier.drawId = _lastCompletedDrawId; + tier.prizeSize = uint96( + _computePrizeSize( + _tier, + _numberOfTiers, + fromUD34x4toUD60x18(tier.prizeTokenPerShare), + fromUD34x4toUD60x18(prizeTokenPerShare) + ) + ); } - - /// @notice Computes the total shares in the system given the number of tiers. That is `(number of tiers * tier shares) + canary shares + reserve shares` - /// @param _numberOfTiers The number of tiers to calculate the total shares for - /// @return The total shares - function _getTotalShares(uint8 _numberOfTiers) internal view returns (uint256) { - return uint256(_numberOfTiers-1) * uint256(tierShares) + uint256(canaryShares) + uint256(reserveShares); + return tier; + } + + /// @notice Computes the total shares in the system. That is `(number of tiers * tier shares) + canary shares + reserve shares` + /// @return The total shares + function getTotalShares() external view returns (uint256) { + return _getTotalShares(numberOfTiers); + } + + /// @notice Computes the total shares in the system given the number of tiers. That is `(number of tiers * tier shares) + canary shares + reserve shares` + /// @param _numberOfTiers The number of tiers to calculate the total shares for + /// @return The total shares + function _getTotalShares(uint8 _numberOfTiers) internal view returns (uint256) { + return + uint256(_numberOfTiers - 1) * + uint256(tierShares) + + uint256(canaryShares) + + uint256(reserveShares); + } + + /// @notice Computes the number of shares for the given tier. If the tier is the canary tier, then the canary shares are returned. Normal tier shares otherwise. + /// @param _tier The tier to request share for + /// @param _numTiers The number of tiers. Passed explicitly as an optimization + /// @return The number of shares for the given tier + function _computeShares(uint8 _tier, uint8 _numTiers) internal view returns (uint8) { + return _isCanaryTier(_tier, _numTiers) ? canaryShares : tierShares; + } + + /// @notice Consumes liquidity from the given tier. + /// @param _tierStruct The tier to consume liquidity from + /// @param _tier The tier number + /// @param _liquidity The amount of liquidity to consume + /// @return An updated Tier struct after consumption + function _consumeLiquidity( + Tier memory _tierStruct, + uint8 _tier, + uint104 _liquidity + ) internal returns (Tier memory) { + uint8 _shares = _computeShares(_tier, numberOfTiers); + uint104 remainingLiquidity = uint104(_remainingTierLiquidity(_tierStruct, _shares)); + if (_liquidity > remainingLiquidity) { + uint104 excess = _liquidity - remainingLiquidity; + if (excess > _reserve) { + revert InsufficientLiquidity(_liquidity); + } + _reserve -= excess; + _tierStruct.prizeTokenPerShare = prizeTokenPerShare; + } else { + UD34x4 delta = fromUD60x18toUD34x4(toUD60x18(_liquidity).div(toUD60x18(_shares))); + _tierStruct.prizeTokenPerShare = UD34x4.wrap( + UD34x4.unwrap(_tierStruct.prizeTokenPerShare) + UD34x4.unwrap(delta) + ); } - - /// @notice Computes the number of shares for the given tier. If the tier is the canary tier, then the canary shares are returned. Normal tier shares otherwise. - /// @param _tier The tier to request share for - /// @param _numTiers The number of tiers. Passed explicitly as an optimization - /// @return The number of shares for the given tier - function _computeShares(uint8 _tier, uint8 _numTiers) internal view returns (uint8) { - return _isCanaryTier(_tier, _numTiers) ? canaryShares : tierShares; - } - - /// @notice Consumes liquidity from the given tier. - /// @param _tierStruct The tier to consume liquidity from - /// @param _tier The tier number - /// @param _liquidity The amount of liquidity to consume - /// @return An updated Tier struct after consumption - function _consumeLiquidity(Tier memory _tierStruct, uint8 _tier, uint104 _liquidity) internal returns (Tier memory) { - uint8 _shares = _computeShares(_tier, numberOfTiers); - uint104 remainingLiquidity = uint104(_remainingTierLiquidity(_tierStruct, _shares)); - if (_liquidity > remainingLiquidity) { - uint104 excess = _liquidity - remainingLiquidity; - if (excess > _reserve) { - revert InsufficientLiquidity(_liquidity); - } - _reserve -= excess; - _tierStruct.prizeTokenPerShare = prizeTokenPerShare; - } else { - UD34x4 delta = fromUD60x18toUD34x4( - toUD60x18(_liquidity).div(toUD60x18(_shares)) - ); - _tierStruct.prizeTokenPerShare = UD34x4.wrap(UD34x4.unwrap(_tierStruct.prizeTokenPerShare) + UD34x4.unwrap(delta)); - } - _tiers[_tier] = _tierStruct; - return _tierStruct; - } - - /// @notice Computes the total liquidity available to a tier - /// @param _tier The tier to compute the liquidity for - /// @return The total liquidity - function _remainingTierLiquidity(Tier memory _tier, uint8 _shares) internal view returns (uint112) { - UD34x4 _prizeTokenPerShare = prizeTokenPerShare; - if (UD34x4.unwrap(_tier.prizeTokenPerShare) >= UD34x4.unwrap(_prizeTokenPerShare)) { - return 0; - } - UD60x18 delta = fromUD34x4toUD60x18(_prizeTokenPerShare).sub(fromUD34x4toUD60x18(_tier.prizeTokenPerShare)); - // delta max int size is (uMAX_UD34x4 / 1e4) - // max share size is 256 - // result max = (uMAX_UD34x4 / 1e4) * 256 - return uint112(fromUD60x18(delta.mul(toUD60x18(_shares)))); - } - - /// @notice Computes the prize size of the given tier - /// @param _tier The tier to compute the prize size of - /// @param _numberOfTiers The current number of tiers - /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct - /// @param _prizeTokenPerShare The global prizeTokenPerShare - /// @return The prize size - function _computePrizeSize(uint8 _tier, uint8 _numberOfTiers, UD60x18 _tierPrizeTokenPerShare, UD60x18 _prizeTokenPerShare) internal view returns (uint256) { - assert(_tier < _numberOfTiers); - uint256 prizeSize; - if (_prizeTokenPerShare.gt(_tierPrizeTokenPerShare)) { - if (_isCanaryTier(_tier, _numberOfTiers)) { - prizeSize = _computePrizeSize(_tierPrizeTokenPerShare, _prizeTokenPerShare, _canaryPrizeCountFractional(_numberOfTiers), canaryShares); - } else { - prizeSize = _computePrizeSize(_tierPrizeTokenPerShare, _prizeTokenPerShare, toUD60x18(TierCalculationLib.prizeCount(_tier)), tierShares); - } - } - return prizeSize; - } - - /// @notice Computes the prize size with the given parameters - /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct - /// @param _prizeTokenPerShare The global prizeTokenPerShare - /// @param _fractionalPrizeCount The prize count as UD60x18 - /// @param _shares The number of shares that the tier has - /// @return The prize size - function _computePrizeSize(UD60x18 _tierPrizeTokenPerShare, UD60x18 _prizeTokenPerShare, UD60x18 _fractionalPrizeCount, uint8 _shares) internal pure returns (uint256) { - return fromUD60x18(_prizeTokenPerShare.sub(_tierPrizeTokenPerShare).mul(toUD60x18(_shares)).div(_fractionalPrizeCount)); + _tiers[_tier] = _tierStruct; + return _tierStruct; + } + + /// @notice Computes the total liquidity available to a tier + /// @param _tier The tier to compute the liquidity for + /// @return The total liquidity + function _remainingTierLiquidity( + Tier memory _tier, + uint8 _shares + ) internal view returns (uint112) { + UD34x4 _prizeTokenPerShare = prizeTokenPerShare; + if (UD34x4.unwrap(_tier.prizeTokenPerShare) >= UD34x4.unwrap(_prizeTokenPerShare)) { + return 0; } - - function _isCanaryTier(uint8 _tier, uint8 _numberOfTiers) internal pure returns (bool) { - return _tier == _numberOfTiers - 1; - } - - /// @notice Reclaims liquidity from tiers, starting at the highest tier - /// @param _numberOfTiers The existing number of tiers - /// @param _nextNumberOfTiers The next number of tiers. Must be less than _numberOfTiers - /// @return The total reclaimed liquidity - function _getTierLiquidityToReclaim(uint8 _numberOfTiers, uint8 _nextNumberOfTiers, UD60x18 _prizeTokenPerShare) internal view returns (uint256) { - UD60x18 reclaimedLiquidity; - // need to redistribute to the canary tier and any new tiers (if expanding) - uint8 start; - uint8 end; - // if we are expanding, need to reset the canary tier and all of the new tiers - if (_nextNumberOfTiers < _numberOfTiers) { - start = _nextNumberOfTiers - 1; - end = _numberOfTiers; - } else { // just reset the canary tier - start = _numberOfTiers - 1; - end = _numberOfTiers; - } - for (uint8 i = start; i < end; i++) { - Tier memory tierLiquidity = _tiers[i]; - uint8 shares = _computeShares(i, _numberOfTiers); - UD60x18 liq = _getRemainingTierLiquidity(shares, fromUD34x4toUD60x18(tierLiquidity.prizeTokenPerShare), _prizeTokenPerShare); - reclaimedLiquidity = reclaimedLiquidity.add(liq); - } - return fromUD60x18(reclaimedLiquidity); - } - - /// @notice Computes the total liquidity available to a tier - /// @param _tier The tier to compute the liquidity for - /// @return The total liquidity - function getRemainingTierLiquidity(uint8 _tier) external view returns (uint256) { - uint8 _numTiers = numberOfTiers; - return fromUD60x18(_getRemainingTierLiquidity(_computeShares(_tier, _numTiers), fromUD34x4toUD60x18(_getTier(_tier, _numTiers).prizeTokenPerShare), fromUD34x4toUD60x18(prizeTokenPerShare))); - } - - /// @notice Computes the remaining tier liquidity - /// @param _shares The number of shares that the tier has (can be tierShares or canaryShares) - /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct - /// @param _prizeTokenPerShare The global prizeTokenPerShare - /// @return The total available liquidity - function _getRemainingTierLiquidity(uint256 _shares, UD60x18 _tierPrizeTokenPerShare, UD60x18 _prizeTokenPerShare) internal pure returns (UD60x18) { - if (_tierPrizeTokenPerShare.gte(_prizeTokenPerShare)) { - return ud(0); - } - UD60x18 delta = _prizeTokenPerShare.sub(_tierPrizeTokenPerShare); - return delta.mul(toUD60x18(_shares)); - } - - /// @notice Retrieves the id of the next draw to be completed. - /// @return The next draw id - function getNextDrawId() external view returns (uint16) { - return lastCompletedDrawId + 1; - } - - /// @notice Estimates the number of prizes that will be awarded - /// @return The estimated prize count - function estimatedPrizeCount() external view returns (uint32) { - return _estimatedPrizeCount(numberOfTiers); - } - - /// @notice Estimates the number of prizes that will be awarded given a number of tiers. - /// @param numTiers The number of tiers - /// @return The estimated prize count for the given number of tiers - function estimatedPrizeCount(uint8 numTiers) external view returns (uint32) { - return _estimatedPrizeCount(numTiers); - } - - /// @notice Returns the number of canary prizes as a fraction. This allows the canary prize size to accurately represent the number of tiers + 1. - /// @param numTiers The number of prize tiers - /// @return The number of canary prizes - function canaryPrizeCountFractional(uint8 numTiers) external view returns (UD60x18) { - return _canaryPrizeCountFractional(numTiers); - } - - /// @notice Computes the number of canary prizes for the last completed draw - function canaryPrizeCount() external view returns (uint32) { - return _canaryPrizeCount(numberOfTiers); + UD60x18 delta = fromUD34x4toUD60x18(_prizeTokenPerShare).sub( + fromUD34x4toUD60x18(_tier.prizeTokenPerShare) + ); + // delta max int size is (uMAX_UD34x4 / 1e4) + // max share size is 256 + // result max = (uMAX_UD34x4 / 1e4) * 256 + return uint112(fromUD60x18(delta.mul(toUD60x18(_shares)))); + } + + /// @notice Computes the prize size of the given tier + /// @param _tier The tier to compute the prize size of + /// @param _numberOfTiers The current number of tiers + /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct + /// @param _prizeTokenPerShare The global prizeTokenPerShare + /// @return The prize size + function _computePrizeSize( + uint8 _tier, + uint8 _numberOfTiers, + UD60x18 _tierPrizeTokenPerShare, + UD60x18 _prizeTokenPerShare + ) internal view returns (uint256) { + assert(_tier < _numberOfTiers); + uint256 prizeSize; + if (_prizeTokenPerShare.gt(_tierPrizeTokenPerShare)) { + if (_isCanaryTier(_tier, _numberOfTiers)) { + prizeSize = _computePrizeSize( + _tierPrizeTokenPerShare, + _prizeTokenPerShare, + _canaryPrizeCountFractional(_numberOfTiers), + canaryShares + ); + } else { + prizeSize = _computePrizeSize( + _tierPrizeTokenPerShare, + _prizeTokenPerShare, + toUD60x18(TierCalculationLib.prizeCount(_tier)), + tierShares + ); + } } - - /// @notice Computes the number of canary prizes for the last completed draw - function _canaryPrizeCount(uint8 _numberOfTiers) internal view returns (uint32) { - return uint32(fromUD60x18(_canaryPrizeCountFractional(_numberOfTiers).floor())); + return prizeSize; + } + + /// @notice Computes the prize size with the given parameters + /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct + /// @param _prizeTokenPerShare The global prizeTokenPerShare + /// @param _fractionalPrizeCount The prize count as UD60x18 + /// @param _shares The number of shares that the tier has + /// @return The prize size + function _computePrizeSize( + UD60x18 _tierPrizeTokenPerShare, + UD60x18 _prizeTokenPerShare, + UD60x18 _fractionalPrizeCount, + uint8 _shares + ) internal pure returns (uint256) { + return + fromUD60x18( + _prizeTokenPerShare.sub(_tierPrizeTokenPerShare).mul(toUD60x18(_shares)).div( + _fractionalPrizeCount + ) + ); + } + + function _isCanaryTier(uint8 _tier, uint8 _numberOfTiers) internal pure returns (bool) { + return _tier == _numberOfTiers - 1; + } + + /// @notice Reclaims liquidity from tiers, starting at the highest tier + /// @param _numberOfTiers The existing number of tiers + /// @param _nextNumberOfTiers The next number of tiers. Must be less than _numberOfTiers + /// @return The total reclaimed liquidity + function _getTierLiquidityToReclaim( + uint8 _numberOfTiers, + uint8 _nextNumberOfTiers, + UD60x18 _prizeTokenPerShare + ) internal view returns (uint256) { + UD60x18 reclaimedLiquidity; + // need to redistribute to the canary tier and any new tiers (if expanding) + uint8 start; + uint8 end; + // if we are expanding, need to reset the canary tier and all of the new tiers + if (_nextNumberOfTiers < _numberOfTiers) { + start = _nextNumberOfTiers - 1; + end = _numberOfTiers; + } else { + // just reset the canary tier + start = _numberOfTiers - 1; + end = _numberOfTiers; } - - /// @notice Computes the number of canary prizes given the number of tiers. - /// @param _numTiers The number of prize tiers - /// @return The number of canary prizes - function canaryPrizeCount(uint8 _numTiers) external view returns (uint32) { - return _canaryPrizeCount(_numTiers); + for (uint8 i = start; i < end; i++) { + Tier memory tierLiquidity = _tiers[i]; + uint8 shares = _computeShares(i, _numberOfTiers); + UD60x18 liq = _getRemainingTierLiquidity( + shares, + fromUD34x4toUD60x18(tierLiquidity.prizeTokenPerShare), + _prizeTokenPerShare + ); + reclaimedLiquidity = reclaimedLiquidity.add(liq); } - - /// @notice Returns the balance of the reserve - /// @return The amount of tokens that have been reserved. - function reserve() external view returns (uint256) { - return _reserve; + return fromUD60x18(reclaimedLiquidity); + } + + /// @notice Computes the total liquidity available to a tier + /// @param _tier The tier to compute the liquidity for + /// @return The total liquidity + function getRemainingTierLiquidity(uint8 _tier) external view returns (uint256) { + uint8 _numTiers = numberOfTiers; + return + fromUD60x18( + _getRemainingTierLiquidity( + _computeShares(_tier, _numTiers), + fromUD34x4toUD60x18(_getTier(_tier, _numTiers).prizeTokenPerShare), + fromUD34x4toUD60x18(prizeTokenPerShare) + ) + ); + } + + /// @notice Computes the remaining tier liquidity + /// @param _shares The number of shares that the tier has (can be tierShares or canaryShares) + /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct + /// @param _prizeTokenPerShare The global prizeTokenPerShare + /// @return The total available liquidity + function _getRemainingTierLiquidity( + uint256 _shares, + UD60x18 _tierPrizeTokenPerShare, + UD60x18 _prizeTokenPerShare + ) internal pure returns (UD60x18) { + if (_tierPrizeTokenPerShare.gte(_prizeTokenPerShare)) { + return ud(0); } - - /// @notice Estimates the prize count for the given tier - /// @param numTiers The number of prize tiers - /// @return The estimated total number of prizes - function _estimatedPrizeCount(uint8 numTiers) internal view returns (uint32) { - if (numTiers == 3) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS; - } else if (numTiers == 4) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS; - } else if (numTiers == 5) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS; - } else if (numTiers == 6) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS; - } else if (numTiers == 7) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS; - } else if (numTiers == 8) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS; - } else if (numTiers == 9) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS; - } else if (numTiers == 10) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS; - } else if (numTiers == 11) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS; - } else if (numTiers == 12) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS; - } else if (numTiers == 13) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS; - } else if (numTiers == 14) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS; - } else if (numTiers == 15) { - return ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS; - } - return 0; + UD60x18 delta = _prizeTokenPerShare.sub(_tierPrizeTokenPerShare); + return delta.mul(toUD60x18(_shares)); + } + + /// @notice Retrieves the id of the next draw to be completed. + /// @return The next draw id + function getNextDrawId() external view returns (uint16) { + return lastCompletedDrawId + 1; + } + + /// @notice Estimates the number of prizes that will be awarded + /// @return The estimated prize count + function estimatedPrizeCount() external view returns (uint32) { + return _estimatedPrizeCount(numberOfTiers); + } + + /// @notice Estimates the number of prizes that will be awarded given a number of tiers. + /// @param numTiers The number of tiers + /// @return The estimated prize count for the given number of tiers + function estimatedPrizeCount(uint8 numTiers) external view returns (uint32) { + return _estimatedPrizeCount(numTiers); + } + + /// @notice Returns the number of canary prizes as a fraction. This allows the canary prize size to accurately represent the number of tiers + 1. + /// @param numTiers The number of prize tiers + /// @return The number of canary prizes + function canaryPrizeCountFractional(uint8 numTiers) external view returns (UD60x18) { + return _canaryPrizeCountFractional(numTiers); + } + + /// @notice Computes the number of canary prizes for the last completed draw + function canaryPrizeCount() external view returns (uint32) { + return _canaryPrizeCount(numberOfTiers); + } + + /// @notice Computes the number of canary prizes for the last completed draw + function _canaryPrizeCount(uint8 _numberOfTiers) internal view returns (uint32) { + return uint32(fromUD60x18(_canaryPrizeCountFractional(_numberOfTiers).floor())); + } + + /// @notice Computes the number of canary prizes given the number of tiers. + /// @param _numTiers The number of prize tiers + /// @return The number of canary prizes + function canaryPrizeCount(uint8 _numTiers) external view returns (uint32) { + return _canaryPrizeCount(_numTiers); + } + + /// @notice Returns the balance of the reserve + /// @return The amount of tokens that have been reserved. + function reserve() external view returns (uint256) { + return _reserve; + } + + /// @notice Estimates the prize count for the given tier + /// @param numTiers The number of prize tiers + /// @return The estimated total number of prizes + function _estimatedPrizeCount(uint8 numTiers) internal view returns (uint32) { + if (numTiers == 3) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_2_TIERS; + } else if (numTiers == 4) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_3_TIERS; + } else if (numTiers == 5) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS; + } else if (numTiers == 6) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS; + } else if (numTiers == 7) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS; + } else if (numTiers == 8) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS; + } else if (numTiers == 9) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS; + } else if (numTiers == 10) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS; + } else if (numTiers == 11) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS; + } else if (numTiers == 12) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS; + } else if (numTiers == 13) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_12_TIERS; + } else if (numTiers == 14) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_13_TIERS; + } else if (numTiers == 15) { + return ESTIMATED_PRIZES_PER_DRAW_FOR_14_TIERS; } - - /// @notice Computes the canary prize count for the given number of tiers - /// @param numTiers The number of prize tiers - /// @return The fractional canary prize count - function _canaryPrizeCountFractional(uint8 numTiers) internal view returns (UD60x18) { - if (numTiers == 3) { - return CANARY_PRIZE_COUNT_FOR_2_TIERS; - } else if (numTiers == 4) { - return CANARY_PRIZE_COUNT_FOR_3_TIERS; - } else if (numTiers == 5) { - return CANARY_PRIZE_COUNT_FOR_4_TIERS; - } else if (numTiers == 6) { - return CANARY_PRIZE_COUNT_FOR_5_TIERS; - } else if (numTiers == 7) { - return CANARY_PRIZE_COUNT_FOR_6_TIERS; - } else if (numTiers == 8) { - return CANARY_PRIZE_COUNT_FOR_7_TIERS; - } else if (numTiers == 9) { - return CANARY_PRIZE_COUNT_FOR_8_TIERS; - } else if (numTiers == 10) { - return CANARY_PRIZE_COUNT_FOR_9_TIERS; - } else if (numTiers == 11) { - return CANARY_PRIZE_COUNT_FOR_10_TIERS; - } else if (numTiers == 12) { - return CANARY_PRIZE_COUNT_FOR_11_TIERS; - } else if (numTiers == 13) { - return CANARY_PRIZE_COUNT_FOR_12_TIERS; - } else if (numTiers == 14) { - return CANARY_PRIZE_COUNT_FOR_13_TIERS; - } else if (numTiers == 15) { - return CANARY_PRIZE_COUNT_FOR_14_TIERS; - } - return ud(0); + return 0; + } + + /// @notice Computes the canary prize count for the given number of tiers + /// @param numTiers The number of prize tiers + /// @return The fractional canary prize count + function _canaryPrizeCountFractional(uint8 numTiers) internal view returns (UD60x18) { + if (numTiers == 3) { + return CANARY_PRIZE_COUNT_FOR_2_TIERS; + } else if (numTiers == 4) { + return CANARY_PRIZE_COUNT_FOR_3_TIERS; + } else if (numTiers == 5) { + return CANARY_PRIZE_COUNT_FOR_4_TIERS; + } else if (numTiers == 6) { + return CANARY_PRIZE_COUNT_FOR_5_TIERS; + } else if (numTiers == 7) { + return CANARY_PRIZE_COUNT_FOR_6_TIERS; + } else if (numTiers == 8) { + return CANARY_PRIZE_COUNT_FOR_7_TIERS; + } else if (numTiers == 9) { + return CANARY_PRIZE_COUNT_FOR_8_TIERS; + } else if (numTiers == 10) { + return CANARY_PRIZE_COUNT_FOR_9_TIERS; + } else if (numTiers == 11) { + return CANARY_PRIZE_COUNT_FOR_10_TIERS; + } else if (numTiers == 12) { + return CANARY_PRIZE_COUNT_FOR_11_TIERS; + } else if (numTiers == 13) { + return CANARY_PRIZE_COUNT_FOR_12_TIERS; + } else if (numTiers == 14) { + return CANARY_PRIZE_COUNT_FOR_13_TIERS; + } else if (numTiers == 15) { + return CANARY_PRIZE_COUNT_FOR_14_TIERS; } - + return ud(0); + } } diff --git a/src/libraries/BitLib.sol b/src/libraries/BitLib.sol index 42230a8..b85322a 100644 --- a/src/libraries/BitLib.sol +++ b/src/libraries/BitLib.sol @@ -4,24 +4,23 @@ pragma solidity 0.8.17; /// @title Helper functions to retrieve on bit from a word of bits. library BitLib { + /// @notice Flips one bit in a packed array of bits. + /// @param packedBits The bit storage. There are 256 bits in a uint256. + /// @param bit The bit to flip + /// @return The passed bit storage that has the desired bit flipped. + function flipBit(uint256 packedBits, uint8 bit) internal pure returns (uint256) { + // create mask + uint256 mask = 0x1 << bit; + return packedBits ^ mask; + } - /// @notice Flips one bit in a packed array of bits. - /// @param packedBits The bit storage. There are 256 bits in a uint256. - /// @param bit The bit to flip - /// @return The passed bit storage that has the desired bit flipped. - function flipBit(uint256 packedBits, uint8 bit) internal pure returns (uint256) { - // create mask - uint256 mask = 0x1 << bit; - return packedBits ^ mask; - } - - /// @notice Retrieves the value of one bit from a packed array of bits. - /// @param packedBits The bit storage. There are 256 bits in a uint256. - /// @param bit The bit to retrieve - /// @return The value of the desired bit - function getBit(uint256 packedBits, uint8 bit) internal pure returns (bool) { - uint256 mask = (0x1 << bit);// ^ type(uint256).max; - // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - return (packedBits & mask) >> bit == 1; - } -} \ No newline at end of file + /// @notice Retrieves the value of one bit from a packed array of bits. + /// @param packedBits The bit storage. There are 256 bits in a uint256. + /// @param bit The bit to retrieve + /// @return The value of the desired bit + function getBit(uint256 packedBits, uint8 bit) internal pure returns (bool) { + uint256 mask = (0x1 << bit); // ^ type(uint256).max; + // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + return (packedBits & mask) >> bit == 1; + } +} diff --git a/src/libraries/DrawAccumulatorLib.sol b/src/libraries/DrawAccumulatorLib.sol index 2d56d43..f42cca7 100644 --- a/src/libraries/DrawAccumulatorLib.sol +++ b/src/libraries/DrawAccumulatorLib.sol @@ -23,161 +23,173 @@ error InvalidDrawRange(uint16 startDrawId, uint16 endDrawId); error InvalidDisbursedEndDrawId(uint16 endDrawId); struct Observation { - // track the total amount available as of this Observation - uint96 available; - // track the total accumulated previously - uint168 disbursed; + // track the total amount available as of this Observation + uint96 available; + // track the total accumulated previously + uint168 disbursed; } /// @title Draw Accumulator Lib /// @author PoolTogether Inc. Team /// @notice This contract distributes tokens over time according to an exponential weighted average. Time is divided into discrete "draws", of which each is allocated tokens. library DrawAccumulatorLib { + /// @notice The maximum number of observations that can be recorded. + uint24 internal constant MAX_CARDINALITY = 366; + + struct RingBufferInfo { + uint16 nextIndex; + uint16 cardinality; + } + + struct Accumulator { + RingBufferInfo ringBufferInfo; + uint16[MAX_CARDINALITY] drawRingBuffer; + mapping(uint256 => Observation) observations; + } + + struct Pair32 { + uint16 first; + uint16 second; + } + + /// @notice Adds balance for the given draw id to the accumulator. + /// @param accumulator The accumulator to add to + /// @param _amount The amount of balance to add + /// @param _drawId The draw id to which to add balance to. This must be greater than or equal to the previous addition's draw id. + /// @param _alpha The alpha value to use for the exponential weighted average. + /// @return True if a new observation was created, false otherwise. + function add( + Accumulator storage accumulator, + uint256 _amount, + uint16 _drawId, + SD59x18 _alpha + ) internal returns (bool) { + if (_drawId == 0) { + revert AddToDrawZero(); + } + RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo; - /// @notice The maximum number of observations that can be recorded. - uint24 internal constant MAX_CARDINALITY = 366; + uint256 newestIndex = RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY); + uint16 newestDrawId_ = accumulator.drawRingBuffer[newestIndex]; - struct RingBufferInfo { - uint16 nextIndex; - uint16 cardinality; + if (_drawId < newestDrawId_) { + revert DrawClosed(_drawId, newestDrawId_); } - struct Accumulator { - RingBufferInfo ringBufferInfo; - uint16[MAX_CARDINALITY] drawRingBuffer; - mapping(uint256 => Observation) observations; + Observation memory newestObservation_ = accumulator.observations[newestDrawId_]; + if (_drawId != newestDrawId_) { + uint256 relativeDraw = _drawId - newestDrawId_; + + uint256 remainingAmount = integrateInf(_alpha, relativeDraw, newestObservation_.available); + uint256 disbursedAmount = integrate(_alpha, 0, relativeDraw, newestObservation_.available); + uint256 remainder = newestObservation_.available - (remainingAmount + disbursedAmount); + + accumulator.drawRingBuffer[ringBufferInfo.nextIndex] = _drawId; + accumulator.observations[_drawId] = Observation({ + available: uint96(_amount + remainingAmount), + disbursed: uint168(newestObservation_.disbursed + disbursedAmount + remainder) + }); + uint16 nextIndex = uint16(RingBufferLib.nextIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY)); + uint16 cardinality = ringBufferInfo.cardinality; + if (ringBufferInfo.cardinality < MAX_CARDINALITY) { + cardinality += 1; + } + accumulator.ringBufferInfo = RingBufferInfo({ + nextIndex: nextIndex, + cardinality: cardinality + }); + return true; + } else { + accumulator.observations[newestDrawId_] = Observation({ + available: uint96(newestObservation_.available + _amount), + disbursed: newestObservation_.disbursed + }); + return false; } - - struct Pair32 { - uint16 first; - uint16 second; + } + + /// @notice Gets the total remaining balance after and including the given start draw id. This is the sum of all draw balances from start draw id to infinity. + /// The start draw id must be greater than or equal to the newest draw id. + /// @param accumulator The accumulator to sum + /// @param _startDrawId The draw id to start summing from, inclusive + /// @param _alpha The alpha value to use for the exponential weighted average + /// @return The sum of draw balances from start draw to infinity + function getTotalRemaining( + Accumulator storage accumulator, + uint16 _startDrawId, + SD59x18 _alpha + ) internal view returns (uint256) { + RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo; + if (ringBufferInfo.cardinality == 0) { + return 0; } - - /// @notice Adds balance for the given draw id to the accumulator. - /// @param accumulator The accumulator to add to - /// @param _amount The amount of balance to add - /// @param _drawId The draw id to which to add balance to. This must be greater than or equal to the previous addition's draw id. - /// @param _alpha The alpha value to use for the exponential weighted average. - /// @return True if a new observation was created, false otherwise. - function add(Accumulator storage accumulator, uint256 _amount, uint16 _drawId, SD59x18 _alpha) internal returns (bool) { - if (_drawId == 0) { - revert AddToDrawZero(); - } - RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo; - - uint256 newestIndex = RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY); - uint16 newestDrawId_ = accumulator.drawRingBuffer[newestIndex]; - - if (_drawId < newestDrawId_) { - revert DrawClosed(_drawId, newestDrawId_); - } - - Observation memory newestObservation_ = accumulator.observations[newestDrawId_]; - if (_drawId != newestDrawId_) { - - uint256 relativeDraw = _drawId - newestDrawId_; - - uint256 remainingAmount = integrateInf(_alpha, relativeDraw, newestObservation_.available); - uint256 disbursedAmount = integrate(_alpha, 0, relativeDraw, newestObservation_.available); - uint256 remainder = newestObservation_.available - (remainingAmount + disbursedAmount); - - accumulator.drawRingBuffer[ringBufferInfo.nextIndex] = _drawId; - accumulator.observations[_drawId] = Observation({ - available: uint96(_amount + remainingAmount), - disbursed: uint168(newestObservation_.disbursed + disbursedAmount + remainder) - }); - uint16 nextIndex = uint16(RingBufferLib.nextIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY)); - uint16 cardinality = ringBufferInfo.cardinality; - if (ringBufferInfo.cardinality < MAX_CARDINALITY) { - cardinality += 1; - } - accumulator.ringBufferInfo = RingBufferInfo({ - nextIndex: nextIndex, - cardinality: cardinality - }); - return true; - } else { - accumulator.observations[newestDrawId_] = Observation({ - available: uint96(newestObservation_.available + _amount), - disbursed: newestObservation_.disbursed - }); - return false; - } + uint256 newestIndex = RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY); + uint16 newestDrawId_ = accumulator.drawRingBuffer[newestIndex]; + if (_startDrawId < newestDrawId_) { + revert DrawClosed(_startDrawId, newestDrawId_); } - - /// @notice Gets the total remaining balance after and including the given start draw id. This is the sum of all draw balances from start draw id to infinity. - /// The start draw id must be greater than or equal to the newest draw id. - /// @param accumulator The accumulator to sum - /// @param _startDrawId The draw id to start summing from, inclusive - /// @param _alpha The alpha value to use for the exponential weighted average - /// @return The sum of draw balances from start draw to infinity - function getTotalRemaining(Accumulator storage accumulator, uint16 _startDrawId, SD59x18 _alpha) internal view returns (uint256) { - RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo; - if (ringBufferInfo.cardinality == 0) { - return 0; - } - uint256 newestIndex = RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY); - uint16 newestDrawId_ = accumulator.drawRingBuffer[newestIndex]; - if (_startDrawId < newestDrawId_) { - revert DrawClosed(_startDrawId, newestDrawId_); - } - Observation memory newestObservation_ = accumulator.observations[newestDrawId_]; - return integrateInf(_alpha, _startDrawId - newestDrawId_, newestObservation_.available); + Observation memory newestObservation_ = accumulator.observations[newestDrawId_]; + return integrateInf(_alpha, _startDrawId - newestDrawId_, newestObservation_.available); + } + + function newestDrawId(Accumulator storage accumulator) internal view returns (uint256) { + return + accumulator.drawRingBuffer[ + RingBufferLib.newestIndex(accumulator.ringBufferInfo.nextIndex, MAX_CARDINALITY) + ]; + } + + /// @notice Retrieves the newest observation from the accumulator + /// @param accumulator The accumulator to retrieve the newest observation from + /// @return The newest observation + function newestObservation( + Accumulator storage accumulator + ) internal view returns (Observation memory) { + return accumulator.observations[newestDrawId(accumulator)]; + } + + /// @notice Gets the balance that was disbursed between the given start and end draw ids, inclusive. + /// This function has an intentional limitation on the value of `_endDrawId` to save gas, but + /// prevents historical disbursement queries that end more than one draw before the last + /// accumulator observation. + /// @param _accumulator The accumulator to get the disbursed balance from + /// @param _startDrawId The start draw id, inclusive + /// @param _endDrawId The end draw id, inclusive (limitation: cannot be more than one draw before the last observed draw) + /// @param _alpha The alpha value to use for the exponential weighted average + /// @return The disbursed balance between the given start and end draw ids, inclusive + function getDisbursedBetween( + Accumulator storage _accumulator, + uint16 _startDrawId, + uint16 _endDrawId, + SD59x18 _alpha + ) internal view returns (uint256) { + if (_startDrawId > _endDrawId) { + revert InvalidDrawRange(_startDrawId, _endDrawId); } - function newestDrawId(Accumulator storage accumulator) internal view returns (uint256) { - return accumulator.drawRingBuffer[RingBufferLib.newestIndex(accumulator.ringBufferInfo.nextIndex, MAX_CARDINALITY)]; - } + RingBufferInfo memory ringBufferInfo = _accumulator.ringBufferInfo; - /// @notice Retrieves the newest observation from the accumulator - /// @param accumulator The accumulator to retrieve the newest observation from - /// @return The newest observation - function newestObservation(Accumulator storage accumulator) internal view returns (Observation memory) { - return accumulator.observations[newestDrawId(accumulator)]; + if (ringBufferInfo.cardinality == 0) { + return 0; } - /// @notice Gets the balance that was disbursed between the given start and end draw ids, inclusive. - /// This function has an intentional limitation on the value of `_endDrawId` to save gas, but - /// prevents historical disbursement queries that end more than one draw before the last - /// accumulator observation. - /// @param _accumulator The accumulator to get the disbursed balance from - /// @param _startDrawId The start draw id, inclusive - /// @param _endDrawId The end draw id, inclusive (limitation: cannot be more than one draw before the last observed draw) - /// @param _alpha The alpha value to use for the exponential weighted average - /// @return The disbursed balance between the given start and end draw ids, inclusive - function getDisbursedBetween( - Accumulator storage _accumulator, - uint16 _startDrawId, - uint16 _endDrawId, - SD59x18 _alpha - ) internal view returns (uint256) { - if (_startDrawId > _endDrawId) { - revert InvalidDrawRange(_startDrawId, _endDrawId); - } - - RingBufferInfo memory ringBufferInfo = _accumulator.ringBufferInfo; - - if (ringBufferInfo.cardinality == 0) { - return 0; - } - - Pair32 memory indexes = computeIndices(ringBufferInfo); - Pair32 memory drawIds = readDrawIds(_accumulator, indexes); - - /** + Pair32 memory indexes = computeIndices(ringBufferInfo); + Pair32 memory drawIds = readDrawIds(_accumulator, indexes); + + /** This check intentionally limits the `_endDrawId` to be no more than one draw before the latest observation. This allows us to make assumptions on the value of `lastObservationDrawIdOccurringAtOrBeforeEnd` and removes the need to run a additional binary search to find it. */ - if (_endDrawId < drawIds.second - 1) { - revert InvalidDisbursedEndDrawId(_endDrawId); - } + if (_endDrawId < drawIds.second - 1) { + revert InvalidDisbursedEndDrawId(_endDrawId); + } - if (_endDrawId < drawIds.first) { - return 0; - } + if (_endDrawId < drawIds.first) { + return 0; + } - /* + /* head: residual accrual from observation before start. (if any) body: if there is more than one observations between start and current, then take the past _accumulator diff @@ -207,194 +219,243 @@ library DrawAccumulatorLib { */ - uint16 lastObservationDrawIdOccurringAtOrBeforeEnd; - if (_endDrawId >= drawIds.second) { - // then it must be the end - lastObservationDrawIdOccurringAtOrBeforeEnd = drawIds.second; - } else { - // otherwise it must be the previous one - lastObservationDrawIdOccurringAtOrBeforeEnd = _accumulator.drawRingBuffer[uint16(RingBufferLib.offset(indexes.second, 1, ringBufferInfo.cardinality))]; - } - - uint16 observationDrawIdBeforeOrAtStart; - uint16 firstObservationDrawIdOccurringAtOrAfterStart; - // if there is only one observation, or startId is after the oldest record - if (_startDrawId >= drawIds.second) { - // then use the last record - observationDrawIdBeforeOrAtStart = drawIds.second; - } else if (_startDrawId <= drawIds.first) { // if the start is before the newest record - // then set to the oldest record. - firstObservationDrawIdOccurringAtOrAfterStart = drawIds.first; - } else { // The start must be between newest and oldest - // binary search - (, observationDrawIdBeforeOrAtStart, , firstObservationDrawIdOccurringAtOrAfterStart) = binarySearch( - _accumulator.drawRingBuffer, indexes.first, indexes.second, ringBufferInfo.cardinality, _startDrawId - ); - } - - uint256 total; - - // if a "head" exists - if (observationDrawIdBeforeOrAtStart > 0 && - firstObservationDrawIdOccurringAtOrAfterStart > 0 && - observationDrawIdBeforeOrAtStart != lastObservationDrawIdOccurringAtOrBeforeEnd) { - Observation memory beforeOrAtStart = _accumulator.observations[observationDrawIdBeforeOrAtStart]; - uint16 headStartDrawId = _startDrawId - observationDrawIdBeforeOrAtStart; - uint16 headEndDrawId = headStartDrawId + (firstObservationDrawIdOccurringAtOrAfterStart - _startDrawId); - uint amount = integrate(_alpha, headStartDrawId, headEndDrawId, beforeOrAtStart.available); - total += amount; - } - - Observation memory atOrBeforeEnd; - // if a "body" exists - if (firstObservationDrawIdOccurringAtOrAfterStart > 0 && - firstObservationDrawIdOccurringAtOrAfterStart < lastObservationDrawIdOccurringAtOrBeforeEnd) { - Observation memory atOrAfterStart = _accumulator.observations[firstObservationDrawIdOccurringAtOrAfterStart]; - atOrBeforeEnd = _accumulator.observations[lastObservationDrawIdOccurringAtOrBeforeEnd]; - uint amount = atOrBeforeEnd.disbursed - atOrAfterStart.disbursed; - total += amount; - } - - total += _computeTail(_accumulator, _startDrawId, _endDrawId, lastObservationDrawIdOccurringAtOrBeforeEnd, _alpha); - - return total; + uint16 lastObservationDrawIdOccurringAtOrBeforeEnd; + if (_endDrawId >= drawIds.second) { + // then it must be the end + lastObservationDrawIdOccurringAtOrBeforeEnd = drawIds.second; + } else { + // otherwise it must be the previous one + lastObservationDrawIdOccurringAtOrBeforeEnd = _accumulator.drawRingBuffer[ + uint16(RingBufferLib.offset(indexes.second, 1, ringBufferInfo.cardinality)) + ]; } - /// @notice Computes the "tail" for the given accumulator and range. The tail is the residual balance from the last observation occurring before the end draw id. - /// @param accumulator The accumulator to compute for - /// @param _startDrawId The start draw id, inclusive - /// @param _endDrawId The end draw id, inclusive - /// @param _lastObservationDrawIdOccurringAtOrBeforeEnd The last observation draw id occurring at or before the end draw id - /// @return The total balance of the tail of the range. - function _computeTail( - Accumulator storage accumulator, - uint16 _startDrawId, - uint16 _endDrawId, - uint16 _lastObservationDrawIdOccurringAtOrBeforeEnd, - SD59x18 _alpha - ) internal view returns (uint256) { - Observation memory lastObservation = accumulator.observations[_lastObservationDrawIdOccurringAtOrBeforeEnd]; - uint16 tailRangeStartDrawId = (_startDrawId > _lastObservationDrawIdOccurringAtOrBeforeEnd ? _startDrawId : _lastObservationDrawIdOccurringAtOrBeforeEnd) - _lastObservationDrawIdOccurringAtOrBeforeEnd; - uint256 amount = integrate(_alpha, tailRangeStartDrawId, _endDrawId - _lastObservationDrawIdOccurringAtOrBeforeEnd + 1, lastObservation.available); - return amount; + uint16 observationDrawIdBeforeOrAtStart; + uint16 firstObservationDrawIdOccurringAtOrAfterStart; + // if there is only one observation, or startId is after the oldest record + if (_startDrawId >= drawIds.second) { + // then use the last record + observationDrawIdBeforeOrAtStart = drawIds.second; + } else if (_startDrawId <= drawIds.first) { + // if the start is before the newest record + // then set to the oldest record. + firstObservationDrawIdOccurringAtOrAfterStart = drawIds.first; + } else { + // The start must be between newest and oldest + // binary search + ( + , + observationDrawIdBeforeOrAtStart, + , + firstObservationDrawIdOccurringAtOrAfterStart + ) = binarySearch( + _accumulator.drawRingBuffer, + indexes.first, + indexes.second, + ringBufferInfo.cardinality, + _startDrawId + ); } - /// @notice Computes the first and last indices of observations for the given ring buffer info - /// @param ringBufferInfo The ring buffer info to compute for - /// @return A pair of indices, where the first is the oldest index and the second is the newest index - function computeIndices(RingBufferInfo memory ringBufferInfo) internal pure returns (Pair32 memory) { - return Pair32({ - first: uint16(RingBufferLib.oldestIndex(ringBufferInfo.nextIndex, ringBufferInfo.cardinality, MAX_CARDINALITY)), - second: uint16(RingBufferLib.newestIndex(ringBufferInfo.nextIndex, ringBufferInfo.cardinality)) - }); - } - - /// @notice Retrieves the draw ids for the given accumulator observation indices - /// @param accumulator The accumulator to retrieve from - /// @param indices The indices to retrieve - /// @return A pair of draw ids, where the first is the draw id of the pair's first index and the second is the draw id of the pair's second index - function readDrawIds(Accumulator storage accumulator, Pair32 memory indices) internal view returns (Pair32 memory) { - return Pair32({ - first: uint16(accumulator.drawRingBuffer[indices.first]), - second: uint16(accumulator.drawRingBuffer[indices.second]) - }); - } - - /// @notice Integrates from the given x to infinity for the exponential weighted average - /// @param _alpha The exponential weighted average smoothing parameter. - /// @param _x The x value to integrate from. - /// @param _k The k value to scale the sum (this is the total available balance). - /// @return The integration from x to inf of the EWA for the given parameters. - function integrateInf(SD59x18 _alpha, uint _x, uint _k) internal pure returns (uint256) { - return uint256(fromSD59x18(computeC(_alpha, _x, _k))); - } + uint256 total; - /// @notice Integrates from the given start x to end x for the exponential weighted average - /// @param _alpha The exponential weighted average smoothing parameter. - /// @param _start The x value to integrate from. - /// @param _end The x value to integrate to - /// @param _k The k value to scale the sum (this is the total available balance). - /// @return The integration from start to end of the EWA for the given parameters. - function integrate(SD59x18 _alpha, uint _start, uint _end, uint _k) internal pure returns (uint256) { - int start = unwrap( - computeC(_alpha, _start, _k) - ); - int end = unwrap( - computeC(_alpha, _end, _k) - ); - return uint256( - fromSD59x18( - sd( - start - - - end - ) - ) - ); + // if a "head" exists + if ( + observationDrawIdBeforeOrAtStart > 0 && + firstObservationDrawIdOccurringAtOrAfterStart > 0 && + observationDrawIdBeforeOrAtStart != lastObservationDrawIdOccurringAtOrBeforeEnd + ) { + Observation memory beforeOrAtStart = _accumulator.observations[ + observationDrawIdBeforeOrAtStart + ]; + uint16 headStartDrawId = _startDrawId - observationDrawIdBeforeOrAtStart; + uint16 headEndDrawId = headStartDrawId + + (firstObservationDrawIdOccurringAtOrAfterStart - _startDrawId); + uint amount = integrate(_alpha, headStartDrawId, headEndDrawId, beforeOrAtStart.available); + total += amount; } - /// @notice Computes the interim value C for the EWA - /// @param _alpha The exponential weighted average smoothing parameter. - /// @param _x The x value to compute for - /// @param _k The total available balance - /// @return The value C - function computeC(SD59x18 _alpha, uint _x, uint _k) internal pure returns (SD59x18) { - return toSD59x18(int(_k)).mul(_alpha.pow(toSD59x18(int256(_x)))); + Observation memory atOrBeforeEnd; + // if a "body" exists + if ( + firstObservationDrawIdOccurringAtOrAfterStart > 0 && + firstObservationDrawIdOccurringAtOrAfterStart < lastObservationDrawIdOccurringAtOrBeforeEnd + ) { + Observation memory atOrAfterStart = _accumulator.observations[ + firstObservationDrawIdOccurringAtOrAfterStart + ]; + atOrBeforeEnd = _accumulator.observations[lastObservationDrawIdOccurringAtOrBeforeEnd]; + uint amount = atOrBeforeEnd.disbursed - atOrAfterStart.disbursed; + total += amount; } - /// @notice Binary searches an array of draw ids for the given target draw id. - /// @param _drawRingBuffer The array of draw ids to search - /// @param _oldestIndex The oldest index in the ring buffer - /// @param _newestIndex The newest index in the ring buffer - /// @param _cardinality The number of items in the ring buffer - /// @param _targetLastCompletedDrawId The target draw id to search for - /// @return beforeOrAtIndex The index of the observation occurring at or before the target draw id - /// @return beforeOrAtDrawId The draw id of the observation occurring at or before the target draw id - /// @return afterOrAtIndex The index of the observation occurring at or after the target draw id - /// @return afterOrAtDrawId The draw id of the observation occurring at or after the target draw id - function binarySearch( - uint16[MAX_CARDINALITY] storage _drawRingBuffer, - uint16 _oldestIndex, - uint16 _newestIndex, - uint16 _cardinality, - uint16 _targetLastCompletedDrawId - ) internal view returns ( - uint16 beforeOrAtIndex, - uint16 beforeOrAtDrawId, - uint16 afterOrAtIndex, - uint16 afterOrAtDrawId - ) { - uint16 leftSide = _oldestIndex; - uint16 rightSide = _newestIndex < leftSide - ? leftSide + _cardinality - 1 - : _newestIndex; - uint16 currentIndex; - - while (true) { - // We start our search in the middle of the `leftSide` and `rightSide`. - // After each iteration, we narrow down the search to the left or the right side while still starting our search in the middle. - currentIndex = (leftSide + rightSide) / 2; - - beforeOrAtIndex = uint16(RingBufferLib.wrap(currentIndex, _cardinality)); - beforeOrAtDrawId = _drawRingBuffer[beforeOrAtIndex]; - - afterOrAtIndex = uint16(RingBufferLib.nextIndex(currentIndex, _cardinality)); - afterOrAtDrawId = _drawRingBuffer[afterOrAtIndex]; - - bool targetAtOrAfter = beforeOrAtDrawId <= _targetLastCompletedDrawId; - - // Check if we've found the corresponding Observation. - if (targetAtOrAfter && _targetLastCompletedDrawId <= afterOrAtDrawId) { - break; - } - - // If `beforeOrAtTimestamp` is greater than `_target`, then we keep searching lower. To the left of the current index. - if (!targetAtOrAfter) { - rightSide = currentIndex - 1; - } else { - // Otherwise, we keep searching higher. To the left of the current index. - leftSide = currentIndex + 1; - } - } + total += _computeTail( + _accumulator, + _startDrawId, + _endDrawId, + lastObservationDrawIdOccurringAtOrBeforeEnd, + _alpha + ); + + return total; + } + + /// @notice Computes the "tail" for the given accumulator and range. The tail is the residual balance from the last observation occurring before the end draw id. + /// @param accumulator The accumulator to compute for + /// @param _startDrawId The start draw id, inclusive + /// @param _endDrawId The end draw id, inclusive + /// @param _lastObservationDrawIdOccurringAtOrBeforeEnd The last observation draw id occurring at or before the end draw id + /// @return The total balance of the tail of the range. + function _computeTail( + Accumulator storage accumulator, + uint16 _startDrawId, + uint16 _endDrawId, + uint16 _lastObservationDrawIdOccurringAtOrBeforeEnd, + SD59x18 _alpha + ) internal view returns (uint256) { + Observation memory lastObservation = accumulator.observations[ + _lastObservationDrawIdOccurringAtOrBeforeEnd + ]; + uint16 tailRangeStartDrawId = ( + _startDrawId > _lastObservationDrawIdOccurringAtOrBeforeEnd + ? _startDrawId + : _lastObservationDrawIdOccurringAtOrBeforeEnd + ) - _lastObservationDrawIdOccurringAtOrBeforeEnd; + uint256 amount = integrate( + _alpha, + tailRangeStartDrawId, + _endDrawId - _lastObservationDrawIdOccurringAtOrBeforeEnd + 1, + lastObservation.available + ); + return amount; + } + + /// @notice Computes the first and last indices of observations for the given ring buffer info + /// @param ringBufferInfo The ring buffer info to compute for + /// @return A pair of indices, where the first is the oldest index and the second is the newest index + function computeIndices( + RingBufferInfo memory ringBufferInfo + ) internal pure returns (Pair32 memory) { + return + Pair32({ + first: uint16( + RingBufferLib.oldestIndex( + ringBufferInfo.nextIndex, + ringBufferInfo.cardinality, + MAX_CARDINALITY + ) + ), + second: uint16( + RingBufferLib.newestIndex(ringBufferInfo.nextIndex, ringBufferInfo.cardinality) + ) + }); + } + + /// @notice Retrieves the draw ids for the given accumulator observation indices + /// @param accumulator The accumulator to retrieve from + /// @param indices The indices to retrieve + /// @return A pair of draw ids, where the first is the draw id of the pair's first index and the second is the draw id of the pair's second index + function readDrawIds( + Accumulator storage accumulator, + Pair32 memory indices + ) internal view returns (Pair32 memory) { + return + Pair32({ + first: uint16(accumulator.drawRingBuffer[indices.first]), + second: uint16(accumulator.drawRingBuffer[indices.second]) + }); + } + + /// @notice Integrates from the given x to infinity for the exponential weighted average + /// @param _alpha The exponential weighted average smoothing parameter. + /// @param _x The x value to integrate from. + /// @param _k The k value to scale the sum (this is the total available balance). + /// @return The integration from x to inf of the EWA for the given parameters. + function integrateInf(SD59x18 _alpha, uint _x, uint _k) internal pure returns (uint256) { + return uint256(fromSD59x18(computeC(_alpha, _x, _k))); + } + + /// @notice Integrates from the given start x to end x for the exponential weighted average + /// @param _alpha The exponential weighted average smoothing parameter. + /// @param _start The x value to integrate from. + /// @param _end The x value to integrate to + /// @param _k The k value to scale the sum (this is the total available balance). + /// @return The integration from start to end of the EWA for the given parameters. + function integrate( + SD59x18 _alpha, + uint _start, + uint _end, + uint _k + ) internal pure returns (uint256) { + int start = unwrap(computeC(_alpha, _start, _k)); + int end = unwrap(computeC(_alpha, _end, _k)); + return uint256(fromSD59x18(sd(start - end))); + } + + /// @notice Computes the interim value C for the EWA + /// @param _alpha The exponential weighted average smoothing parameter. + /// @param _x The x value to compute for + /// @param _k The total available balance + /// @return The value C + function computeC(SD59x18 _alpha, uint _x, uint _k) internal pure returns (SD59x18) { + return toSD59x18(int(_k)).mul(_alpha.pow(toSD59x18(int256(_x)))); + } + + /// @notice Binary searches an array of draw ids for the given target draw id. + /// @param _drawRingBuffer The array of draw ids to search + /// @param _oldestIndex The oldest index in the ring buffer + /// @param _newestIndex The newest index in the ring buffer + /// @param _cardinality The number of items in the ring buffer + /// @param _targetLastCompletedDrawId The target draw id to search for + /// @return beforeOrAtIndex The index of the observation occurring at or before the target draw id + /// @return beforeOrAtDrawId The draw id of the observation occurring at or before the target draw id + /// @return afterOrAtIndex The index of the observation occurring at or after the target draw id + /// @return afterOrAtDrawId The draw id of the observation occurring at or after the target draw id + function binarySearch( + uint16[MAX_CARDINALITY] storage _drawRingBuffer, + uint16 _oldestIndex, + uint16 _newestIndex, + uint16 _cardinality, + uint16 _targetLastCompletedDrawId + ) + internal + view + returns ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) + { + uint16 leftSide = _oldestIndex; + uint16 rightSide = _newestIndex < leftSide ? leftSide + _cardinality - 1 : _newestIndex; + uint16 currentIndex; + + while (true) { + // We start our search in the middle of the `leftSide` and `rightSide`. + // After each iteration, we narrow down the search to the left or the right side while still starting our search in the middle. + currentIndex = (leftSide + rightSide) / 2; + + beforeOrAtIndex = uint16(RingBufferLib.wrap(currentIndex, _cardinality)); + beforeOrAtDrawId = _drawRingBuffer[beforeOrAtIndex]; + + afterOrAtIndex = uint16(RingBufferLib.nextIndex(currentIndex, _cardinality)); + afterOrAtDrawId = _drawRingBuffer[afterOrAtIndex]; + + bool targetAtOrAfter = beforeOrAtDrawId <= _targetLastCompletedDrawId; + + // Check if we've found the corresponding Observation. + if (targetAtOrAfter && _targetLastCompletedDrawId <= afterOrAtDrawId) { + break; + } + + // If `beforeOrAtTimestamp` is greater than `_target`, then we keep searching lower. To the left of the current index. + if (!targetAtOrAfter) { + rightSide = currentIndex - 1; + } else { + // Otherwise, we keep searching higher. To the left of the current index. + leftSide = currentIndex + 1; + } } + } } diff --git a/src/libraries/TierCalculationLib.sol b/src/libraries/TierCalculationLib.sol index dcb19bc..eb6b6e1 100644 --- a/src/libraries/TierCalculationLib.sol +++ b/src/libraries/TierCalculationLib.sol @@ -9,79 +9,91 @@ import { UD60x18, toUD60x18, fromUD60x18 } from "prb-math/UD60x18.sol"; /// @author PoolTogether Inc. Team /// @notice Provides helper functions to assist in calculating tier prize counts, frequency, and odds. library TierCalculationLib { + /// @notice Calculates the odds of a tier occurring + /// @param _tier The tier to calculate odds for + /// @param _numberOfTiers The total number of tiers + /// @param _grandPrizePeriod The number of draws between grand prizes + /// @return The odds that a tier should occur for a single draw. + function getTierOdds( + uint8 _tier, + uint8 _numberOfTiers, + uint16 _grandPrizePeriod + ) internal pure returns (SD59x18) { + SD59x18 _k = sd(1).div(sd(int16(_grandPrizePeriod))).ln().div( + sd((-1 * int8(_numberOfTiers) + 1)) + ); - /// @notice Calculates the odds of a tier occurring - /// @param _tier The tier to calculate odds for - /// @param _numberOfTiers The total number of tiers - /// @param _grandPrizePeriod The number of draws between grand prizes - /// @return The odds that a tier should occur for a single draw. - function getTierOdds(uint8 _tier, uint8 _numberOfTiers, uint16 _grandPrizePeriod) internal pure returns (SD59x18) { - SD59x18 _k = sd(1).div( - sd(int16(_grandPrizePeriod)) - ).ln().div( - sd((-1 * int8(_numberOfTiers) + 1)) - ); + return E.pow(_k.mul(sd(int8(_tier) - (int8(_numberOfTiers) - 1)))); + } - return E.pow(_k.mul(sd(int8(_tier) - (int8(_numberOfTiers) - 1)))); - } + /// @notice Estimates the number of draws between a tier occurring + /// @param _tier The tier to calculate the frequency of + /// @param _numberOfTiers The total number of tiers + /// @param _grandPrizePeriod The number of draws between grand prizes + /// @return The estimated number of draws between the tier occurring + function estimatePrizeFrequencyInDraws( + uint8 _tier, + uint8 _numberOfTiers, + uint16 _grandPrizePeriod + ) internal pure returns (uint256) { + return + uint256( + fromSD59x18( + sd(1e18) + .div(TierCalculationLib.getTierOdds(_tier, _numberOfTiers, _grandPrizePeriod)) + .ceil() + ) + ); + } - /// @notice Estimates the number of draws between a tier occurring - /// @param _tier The tier to calculate the frequency of - /// @param _numberOfTiers The total number of tiers - /// @param _grandPrizePeriod The number of draws between grand prizes - /// @return The estimated number of draws between the tier occurring - function estimatePrizeFrequencyInDraws(uint8 _tier, uint8 _numberOfTiers, uint16 _grandPrizePeriod) internal pure returns (uint256) { - return uint256(fromSD59x18( - sd(1e18).div(TierCalculationLib.getTierOdds(_tier, _numberOfTiers, _grandPrizePeriod)).ceil() - )); - } + /// @notice Computes the number of prizes for a given tier + /// @param _tier The tier to compute for + /// @return The number of prizes + function prizeCount(uint8 _tier) internal pure returns (uint256) { + uint256 _numberOfPrizes = 4 ** _tier; - /// @notice Computes the number of prizes for a given tier - /// @param _tier The tier to compute for - /// @return The number of prizes - function prizeCount(uint8 _tier) internal pure returns (uint256) { - uint256 _numberOfPrizes = 4 ** _tier; + return _numberOfPrizes; + } - return _numberOfPrizes; - } + /// @notice Computes the number of canary prizes as a fraction, based on the share distribution. This is important because the canary prizes should be indicative of the smallest prizes if + /// the number of prize tiers was to increase by 1. + /// @param _numberOfTiers The number of tiers + /// @param _canaryShares The number of shares allocated to canary prizes + /// @param _reserveShares The number of shares allocated to the reserve + /// @param _tierShares The number of shares allocated to prize tiers + /// @return The number of canary prizes, including fractional prizes. + function canaryPrizeCount( + uint8 _numberOfTiers, + uint8 _canaryShares, + uint8 _reserveShares, + uint8 _tierShares + ) internal pure returns (UD60x18) { + uint256 numerator = uint256(_canaryShares) * + ((_numberOfTiers + 1) * uint256(_tierShares) + _canaryShares + _reserveShares); + uint256 denominator = uint256(_tierShares) * + ((_numberOfTiers) * uint256(_tierShares) + _canaryShares + _reserveShares); + UD60x18 multiplier = toUD60x18(numerator).div(toUD60x18(denominator)); + return multiplier.mul(toUD60x18(prizeCount(_numberOfTiers))); + } - /// @notice Computes the number of canary prizes as a fraction, based on the share distribution. This is important because the canary prizes should be indicative of the smallest prizes if - /// the number of prize tiers was to increase by 1. - /// @param _numberOfTiers The number of tiers - /// @param _canaryShares The number of shares allocated to canary prizes - /// @param _reserveShares The number of shares allocated to the reserve - /// @param _tierShares The number of shares allocated to prize tiers - /// @return The number of canary prizes, including fractional prizes. - function canaryPrizeCount( - uint8 _numberOfTiers, - uint8 _canaryShares, - uint8 _reserveShares, - uint8 _tierShares - ) internal pure returns (UD60x18) { - uint256 numerator = uint256(_canaryShares) * ((_numberOfTiers+1) * uint256(_tierShares) + _canaryShares + _reserveShares); - uint256 denominator = uint256(_tierShares) * ((_numberOfTiers) * uint256(_tierShares) + _canaryShares + _reserveShares); - UD60x18 multiplier = toUD60x18(numerator).div(toUD60x18(denominator)); - return multiplier.mul(toUD60x18(prizeCount(_numberOfTiers))); + /// @notice Determines if a user won a prize tier + /// @param _userSpecificRandomNumber The random number to use as entropy + /// @param _userTwab The user's time weighted average balance + /// @param _vaultTwabTotalSupply The vault's time weighted average total supply + /// @param _vaultContributionFraction The portion of the prize that was contributed by the vault + /// @param _tierOdds The odds of the tier occurring + /// @return True if the user won the tier, false otherwise + function isWinner( + uint256 _userSpecificRandomNumber, + uint128 _userTwab, + uint128 _vaultTwabTotalSupply, + SD59x18 _vaultContributionFraction, + SD59x18 _tierOdds + ) internal pure returns (bool) { + if (_vaultTwabTotalSupply == 0) { + return false; } - - /// @notice Determines if a user won a prize tier - /// @param _userSpecificRandomNumber The random number to use as entropy - /// @param _userTwab The user's time weighted average balance - /// @param _vaultTwabTotalSupply The vault's time weighted average total supply - /// @param _vaultContributionFraction The portion of the prize that was contributed by the vault - /// @param _tierOdds The odds of the tier occurring - /// @return True if the user won the tier, false otherwise - function isWinner( - uint256 _userSpecificRandomNumber, - uint128 _userTwab, - uint128 _vaultTwabTotalSupply, - SD59x18 _vaultContributionFraction, - SD59x18 _tierOdds - ) internal pure returns (bool) { - if (_vaultTwabTotalSupply == 0) { - return false; - } - /* + /* The user-held portion of the total supply is the "winning zone". If the above pseudo-random number falls within the winning zone, the user has won this tier However, we scale the size of the zone based on: @@ -89,52 +101,60 @@ library TierCalculationLib { - Number of prizes - Portion of prize that was contributed by the vault */ - // first constrain the random number to be within the vault total supply - uint256 constrainedRandomNumber = _userSpecificRandomNumber % (_vaultTwabTotalSupply); - uint256 winningZone = calculateWinningZone(_userTwab, _vaultContributionFraction, _tierOdds); + // first constrain the random number to be within the vault total supply + uint256 constrainedRandomNumber = _userSpecificRandomNumber % (_vaultTwabTotalSupply); + uint256 winningZone = calculateWinningZone(_userTwab, _vaultContributionFraction, _tierOdds); - return constrainedRandomNumber < winningZone; - } + return constrainedRandomNumber < winningZone; + } - /// @notice Calculates a pseudo-random number that is unique to the user, tier, and winning random number - /// @param _user The user - /// @param _tier The tier - /// @param _prizeIndex The particular prize index they are checking - /// @param _winningRandomNumber The winning random number - /// @return A pseudo-random number - function calculatePseudoRandomNumber( - address _user, - uint8 _tier, - uint32 _prizeIndex, - uint256 _winningRandomNumber - ) internal pure returns (uint256) { - return uint256(keccak256(abi.encode(_user, _tier, _prizeIndex, _winningRandomNumber))); - } + /// @notice Calculates a pseudo-random number that is unique to the user, tier, and winning random number + /// @param _user The user + /// @param _tier The tier + /// @param _prizeIndex The particular prize index they are checking + /// @param _winningRandomNumber The winning random number + /// @return A pseudo-random number + function calculatePseudoRandomNumber( + address _user, + uint8 _tier, + uint32 _prizeIndex, + uint256 _winningRandomNumber + ) internal pure returns (uint256) { + return uint256(keccak256(abi.encode(_user, _tier, _prizeIndex, _winningRandomNumber))); + } - /// @notice Calculates the winning zone for a user. If their pseudo-random number falls within this zone, they win the tier. - /// @param _userTwab The user's time weighted average balance - /// @param _vaultContributionFraction The portion of the prize that was contributed by the vault - /// @param _tierOdds The odds of the tier occurring - /// @return The winning zone for the user. - function calculateWinningZone( - uint256 _userTwab, - SD59x18 _vaultContributionFraction, - SD59x18 _tierOdds - ) internal pure returns (uint256) { - return uint256(fromSD59x18( - toSD59x18(int256(_userTwab)).mul(_tierOdds).mul(_vaultContributionFraction) - )); - } + /// @notice Calculates the winning zone for a user. If their pseudo-random number falls within this zone, they win the tier. + /// @param _userTwab The user's time weighted average balance + /// @param _vaultContributionFraction The portion of the prize that was contributed by the vault + /// @param _tierOdds The odds of the tier occurring + /// @return The winning zone for the user. + function calculateWinningZone( + uint256 _userTwab, + SD59x18 _vaultContributionFraction, + SD59x18 _tierOdds + ) internal pure returns (uint256) { + return + uint256( + fromSD59x18(toSD59x18(int256(_userTwab)).mul(_tierOdds).mul(_vaultContributionFraction)) + ); + } - /// @notice Computes the estimated number of prizes per draw given the number of tiers and the grand prize period. - /// @param _numberOfTiers The number of tiers - /// @param _grandPrizePeriod The grand prize period - /// @return The estimated number of prizes per draw - function estimatedClaimCount(uint8 _numberOfTiers, uint16 _grandPrizePeriod) internal pure returns (uint32) { - uint32 count = 0; - for (uint8 i = 0; i < _numberOfTiers; i++) { - count += uint32(uint256(unwrap(sd(int256(prizeCount(i))).mul(getTierOdds(i, _numberOfTiers, _grandPrizePeriod))))); - } - return count; + /// @notice Computes the estimated number of prizes per draw given the number of tiers and the grand prize period. + /// @param _numberOfTiers The number of tiers + /// @param _grandPrizePeriod The grand prize period + /// @return The estimated number of prizes per draw + function estimatedClaimCount( + uint8 _numberOfTiers, + uint16 _grandPrizePeriod + ) internal pure returns (uint32) { + uint32 count = 0; + for (uint8 i = 0; i < _numberOfTiers; i++) { + count += uint32( + uint256( + unwrap(sd(int256(prizeCount(i))).mul(getTierOdds(i, _numberOfTiers, _grandPrizePeriod))) + ) + ); } + return count; + } } diff --git a/src/libraries/UD34x4.sol b/src/libraries/UD34x4.sol index 2cec499..45c9066 100644 --- a/src/libraries/UD34x4.sol +++ b/src/libraries/UD34x4.sol @@ -19,19 +19,19 @@ uint128 constant uUNIT = 1e4; /// @dev Requirements: /// - x must be less than or equal to `uMAX_UD2x18`. function intoUD60x18(UD34x4 x) pure returns (UD60x18 result) { - uint256 xUint = uint256(UD34x4.unwrap(x)) * uint256(1e14); - result = UD60x18.wrap(xUint); + uint256 xUint = uint256(UD34x4.unwrap(x)) * uint256(1e14); + result = UD60x18.wrap(xUint); } /// @notice Casts an UD34x4 number into UD60x18. /// @dev Requirements: /// - x must be less than or equal to `uMAX_UD2x18`. function fromUD60x18(UD60x18 x) pure returns (UD34x4 result) { - uint256 xUint = UD60x18.unwrap(x) / 1e14; - if (xUint > uMAX_UD34x4) { - revert PRBMath_UD34x4_fromUD60x18_Convert_Overflow(x.unwrap()); - } - result = UD34x4.wrap(uint128(xUint)); + uint256 xUint = UD60x18.unwrap(x) / 1e14; + if (xUint > uMAX_UD34x4) { + revert PRBMath_UD34x4_fromUD60x18_Convert_Overflow(x.unwrap()); + } + result = UD34x4.wrap(uint128(xUint)); } /// @notice Converts an UD34x4 number to a simple integer by dividing it by `UNIT`. Rounds towards zero in the process. @@ -39,7 +39,7 @@ function fromUD60x18(UD60x18 x) pure returns (UD34x4 result) { /// @param x The UD34x4 number to convert. /// @return result The same number in basic integer form. function convert(UD34x4 x) pure returns (uint128 result) { - result = UD34x4.unwrap(x) / uUNIT; + result = UD34x4.unwrap(x) / uUNIT; } /// @notice Converts a simple integer to UD34x4 by multiplying it by `UNIT`. @@ -50,22 +50,22 @@ function convert(UD34x4 x) pure returns (uint128 result) { /// @param x The basic integer to convert. /// @param result The same number converted to UD34x4. function convert(uint128 x) pure returns (UD34x4 result) { - if (x > uMAX_UD34x4 / uUNIT) { - revert PRBMath_UD34x4_Convert_Overflow(x); - } - unchecked { - result = UD34x4.wrap(x * uUNIT); - } + if (x > uMAX_UD34x4 / uUNIT) { + revert PRBMath_UD34x4_Convert_Overflow(x); + } + unchecked { + result = UD34x4.wrap(x * uUNIT); + } } /// @notice Alias for the `convert` function defined above. /// @dev Here for backward compatibility. Will be removed in V4. function fromUD34x4(UD34x4 x) pure returns (uint128 result) { - result = convert(x); + result = convert(x); } /// @notice Alias for the `convert` function defined above. /// @dev Here for backward compatibility. Will be removed in V4. function toUD34x4(uint128 x) pure returns (UD34x4 result) { - result = convert(x); + result = convert(x); } diff --git a/test/PrizePool.t.sol b/test/PrizePool.t.sol index e973c1a..94d716a 100644 --- a/test/PrizePool.t.sol +++ b/test/PrizePool.t.sol @@ -12,167 +12,135 @@ import { UD2x18, ud2x18 } from "prb-math/UD2x18.sol"; import { SD1x18, sd1x18 } from "prb-math/SD1x18.sol"; import { TwabController } from "v5-twab-controller/TwabController.sol"; -import { - PrizePool, - ConstructorParams, - InsufficientRewardsError, - AlreadyClaimedPrize, - DidNotWin, - FeeTooLarge, - SmoothingGTEOne, - ContributionGTDeltaBalance, - InsufficientReserve, - RandomNumberIsZero, - DrawNotFinished, - WinnerPrizeMismatch, - InvalidPrizeIndex, - NoCompletedDraw, - InvalidTier, - DrawManagerAlreadySet, - CallerNotDrawManager -} from "../src/PrizePool.sol"; +import { PrizePool, ConstructorParams, InsufficientRewardsError, AlreadyClaimedPrize, DidNotWin, FeeTooLarge, SmoothingGTEOne, ContributionGTDeltaBalance, InsufficientReserve, RandomNumberIsZero, DrawNotFinished, WinnerPrizeMismatch, InvalidPrizeIndex, NoCompletedDraw, InvalidTier, DrawManagerAlreadySet, CallerNotDrawManager } from "../src/PrizePool.sol"; import { ERC20Mintable } from "./mocks/ERC20Mintable.sol"; contract PrizePoolTest is Test { - PrizePool public prizePool; - - ERC20Mintable public prizeToken; - - address public vault; - - TwabController public twabController; - - address sender1 = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990; - address sender2 = 0x4008Ed96594b645f057c9998a2924545fAbB6545; - address sender3 = 0x796486EBd82E427901511d130Ece93b94f06a980; - address sender4 = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - address sender5 = 0x9ebC8E61f87A301fF25a606d7C06150f856F24E2; - address sender6 = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; - - uint64 lastCompletedDrawStartedAt; - uint32 drawPeriodSeconds; - uint256 winningRandomNumber = 123456; - uint256 startTimestamp = 1000 days; - - /********************************************************************************** - * Events copied from PrizePool.sol - **********************************************************************************/ - /// @notice Emitted when a draw is completed. - /// @param drawId The ID of the draw that was claimed - /// @param winningRandomNumber The winning random number for the completed draw - /// @param numTiers The number of prize tiers in the completed draw - /// @param nextNumTiers The number of tiers for the next draw - event DrawCompleted( - uint16 indexed drawId, - uint256 winningRandomNumber, - uint8 numTiers, - uint8 nextNumTiers + PrizePool public prizePool; + + ERC20Mintable public prizeToken; + + address public vault; + + TwabController public twabController; + + address sender1 = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990; + address sender2 = 0x4008Ed96594b645f057c9998a2924545fAbB6545; + address sender3 = 0x796486EBd82E427901511d130Ece93b94f06a980; + address sender4 = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + address sender5 = 0x9ebC8E61f87A301fF25a606d7C06150f856F24E2; + address sender6 = 0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5; + + uint64 lastCompletedDrawStartedAt; + uint32 drawPeriodSeconds; + uint256 winningRandomNumber = 123456; + uint256 startTimestamp = 1000 days; + + /********************************************************************************** + * Events copied from PrizePool.sol + **********************************************************************************/ + /// @notice Emitted when a draw is completed. + /// @param drawId The ID of the draw that was claimed + /// @param winningRandomNumber The winning random number for the completed draw + /// @param numTiers The number of prize tiers in the completed draw + /// @param nextNumTiers The number of tiers for the next draw + event DrawCompleted( + uint16 indexed drawId, + uint256 winningRandomNumber, + uint8 numTiers, + uint8 nextNumTiers + ); + + /// @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 a vault contributes prize tokens to the pool. + /// @param vault The address of the vault that is contributing tokens + /// @param drawId The ID of the first draw that the tokens will be applied to + /// @param amount The amount of tokens contributed + event ContributePrizeTokens(address indexed vault, uint16 indexed drawId, uint256 amount); + + /// @notice Emitted when an address withdraws their claim 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); + + /// @notice Emitted when the drawManager is set + /// @param drawManager The draw manager + event DrawManagerSet(address indexed drawManager); + + /**********************************************************************************/ + + ConstructorParams params; + + function setUp() public { + vm.warp(startTimestamp); + + prizeToken = new ERC20Mintable("PoolTogether POOL token", "POOL"); + twabController = new TwabController(); + + lastCompletedDrawStartedAt = uint64(block.timestamp + 1 days); // set draw start 1 day into future + drawPeriodSeconds = 1 days; + + address drawManager = address(this); + + params = ConstructorParams( + prizeToken, + twabController, + drawManager, + uint16(365), + drawPeriodSeconds, + lastCompletedDrawStartedAt, + uint8(3), // minimum number of tiers + 100, + 10, + 10, + ud2x18(0.9e18), // claim threshold of 90% + sd1x18(0.9e18) // alpha ); - /// @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 a vault contributes prize tokens to the pool. - /// @param vault The address of the vault that is contributing tokens - /// @param drawId The ID of the first draw that the tokens will be applied to - /// @param amount The amount of tokens contributed - event ContributePrizeTokens( - address indexed vault, - uint16 indexed drawId, - uint256 amount - ); - - /// @notice Emitted when an address withdraws their claim 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 - ); - - - /// @notice Emitted when the drawManager is set - /// @param drawManager The draw manager - event DrawManagerSet( - address indexed drawManager - ); - - /**********************************************************************************/ - - ConstructorParams params; - - function setUp() public { - vm.warp(startTimestamp); - - prizeToken = new ERC20Mintable("PoolTogether POOL token", "POOL"); - twabController = new TwabController(); - - lastCompletedDrawStartedAt = uint64(block.timestamp + 1 days); // set draw start 1 day into future - drawPeriodSeconds = 1 days; - - address drawManager = address(this); - - params = ConstructorParams( - prizeToken, - twabController, - drawManager, - uint16(365), - drawPeriodSeconds, - lastCompletedDrawStartedAt, - uint8(3), // minimum number of tiers - 100, - 10, - 10, - ud2x18(0.9e18), // claim threshold of 90% - sd1x18(0.9e18) // alpha - ); - - vm.expectEmit(); - emit DrawManagerSet(drawManager); - prizePool = new PrizePool(params); - - vault = address(this); - } - - function testConstructor_SmoothingGTEOne() public { - params.smoothing = sd1x18(1.0e18); // smoothing - vm.expectRevert(abi.encodeWithSelector(SmoothingGTEOne.selector, 1000000000000000000)); - new PrizePool(params); - } - - function testReserve_noRemainder() public { - contribute(220e18); - completeAndStartNextDraw(winningRandomNumber); - - // reserve + remainder - assertEq(prizePool.reserve(), 1e18); - } - - function testReserve_withRemainder() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - // reserve + remainder - assertEq(prizePool.reserve(), 0.45454545454545466e18); - } - - function testReserveForNextDraw_noDraw() public { - contribute(100e18); - assertEq(prizePool.reserveForNextDraw(), 0.45454545454545466e18); - } - - function testReserveForNextDraw_existingDraw() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - contribute(100e18); - - /* + vm.expectEmit(); + emit DrawManagerSet(drawManager); + prizePool = new PrizePool(params); + + vault = address(this); + } + + function testConstructor_SmoothingGTEOne() public { + params.smoothing = sd1x18(1.0e18); // smoothing + vm.expectRevert(abi.encodeWithSelector(SmoothingGTEOne.selector, 1000000000000000000)); + new PrizePool(params); + } + + function testReserve_noRemainder() public { + contribute(220e18); + completeAndStartNextDraw(winningRandomNumber); + + // reserve + remainder + assertEq(prizePool.reserve(), 1e18); + } + + function testReserve_withRemainder() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + // reserve + remainder + assertEq(prizePool.reserve(), 0.45454545454545466e18); + } + + function testReserveForNextDraw_noDraw() public { + contribute(100e18); + assertEq(prizePool.reserveForNextDraw(), 0.45454545454545466e18); + } + + function testReserveForNextDraw_existingDraw() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + contribute(100e18); + + /* prev canary: 0.454545454545454546e18 prev reserve: 0.454545454545454546e18 current reserve: 10/220e18 * 9e18 = 0.409090909090909091e18 @@ -180,810 +148,878 @@ contract PrizePoolTest is Test { 0.454545454545454546e18 + 0.454545454545454546e18 + 0.409090909090909091e18 = 1.318181818181818182e18 */ - assertEq(prizePool.reserveForNextDraw(), 1.318181818181818310e18); - } - - function testWithdrawReserve_notManager() public { - vm.prank(address(0)); - vm.expectRevert(abi.encodeWithSelector(CallerNotDrawManager.selector, address(0), address(this))); - prizePool.withdrawReserve(address(0), 1); - } - - function testWithdrawReserve_insuff() public { - vm.expectRevert(abi.encodeWithSelector(InsufficientReserve.selector, 1, 0)); - prizePool.withdrawReserve(address(this), 1); - } - - function testWithdrawReserve() public { - contribute(220e18); - completeAndStartNextDraw(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); - } - - function testCanaryPrizeCount_noParam() public { - assertEq(prizePool.canaryPrizeCount(), 2); - } - - function testCanaryPrizeCount_param() public { - assertEq(prizePool.canaryPrizeCount(4), 8); - } - - function testGetTotalContributedBetween() public { - contribute(10e18); - assertEq(prizePool.getTotalContributedBetween(1, 1), 1e18); - } - - function testGetContributedBetween() public { - contribute(10e18); - assertEq(prizePool.getContributedBetween(address(this), 1, 1), 1e18); - } - - function testGetTierAccrualDurationInDraws() public { - assertEq(prizePool.getTierAccrualDurationInDraws(0), 366); - } - - function testGetTierPrizeCount() public { - assertEq(prizePool.getTierPrizeCount(3), 4**3); - } - - function testContributePrizeTokens() public { - contribute(100); - assertEq(prizeToken.balanceOf(address(prizePool)), 100); - } - - function testContributePrizeTokens_emitsEvent() public { - prizeToken.mint(address(prizePool), 100); - vm.expectEmit(); - emit ContributePrizeTokens(address(this), 1, 100); - prizePool.contributePrizeTokens(address(this), 100); - } - - function testContributePrizeTokens_emitsContributionGTDeltaBalance() public { - vm.expectRevert(abi.encodeWithSelector(ContributionGTDeltaBalance.selector, 100, 0)); - prizePool.contributePrizeTokens(address(this), 100); - } - - function testAccountedBalance_withdrawnReserve() public { - contribute(100e18); - completeAndStartNextDraw(1); - assertEq(prizePool.reserve(), 0.45454545454545466e18); - prizePool.withdrawReserve(address(this), uint104(prizePool.reserve())); - assertEq(prizePool.accountedBalance(), prizeToken.balanceOf(address(prizePool))); - assertEq(prizePool.reserve(), 0); - } - - function testAccountedBalance_noClaims() public { - contribute(100); - assertEq(prizePool.accountedBalance(), 100); - } - - function testAccountedBalance_oneClaim() public { - contribute(100e18); - completeAndStartNextDraw(1); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - assertEq(prizePool.accountedBalance(), 95.4545454545454546e18); - } - - function testAccountedBalance_oneClaim_andMoreContrib() public { - contribute(100e18); - completeAndStartNextDraw(1); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - contribute(10e18); - assertEq(prizePool.accountedBalance(), 105.4545454545454546e18); - } - - function testAccountedBalance_twoClaims() public { - contribute(100e18); - completeAndStartNextDraw(1); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - // 10e18 - 4.5454545454545454e18 = 5.4545454545454546e18 - completeAndStartNextDraw(1); - - // 9e18*100/220 = 4.0909090909090909e18 - assertEq(prizePool.accountedBalance(), 95.4545454545454546e18, "accounted balance"); - - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - } - - function testGetVaultPortion_WhenEmpty() public { - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 0)), 0); - } - - function testGetVaultPortion_WhenOne() public { - contribute(100e18); // available draw 1 - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 1, 1)), 1e18); - } - - function testGetVaultPortion_WhenTwo() public { - contribute(100e18); // available draw 1 - contribute(100e18, address(sender1)); // available draw 1 - - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 1, 1)), 0.5e18); - } - - function testGetVaultPortion_WhenTwo_AccrossTwoDraws() public { - contribute(100e18); - contribute(100e18, address(sender1)); - - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 1, 2)), 0.5e18); - } - - function testGetVaultPortion_BeforeContribution() public { - contribute(100e18); // available on draw 1 - - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 0)), 0); - } - - function testGetVaultPortion_BeforeContributionOnDraw3() public { - completeAndStartNextDraw(winningRandomNumber); // draw 1 - completeAndStartNextDraw(winningRandomNumber); // draw 2 - assertEq(prizePool.getLastCompletedDrawId(), 2); - contribute(100e18); // available on draw 3 - - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 2, 2)), 0); - } - - function testGetVaultPortion_BeforeAndAtContribution() public { - contribute(100e18); // available draw 1 - - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 1)), 1e18); - } - - function testGetVaultPortion_BeforeAndAfterContribution() public { - completeAndStartNextDraw(winningRandomNumber); // draw 1 - contribute(100e18); // available draw 2 - - assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 2)), 1e18); - } - - function testGetNextDrawId() public { - uint256 nextDrawId = prizePool.getNextDrawId(); - assertEq(nextDrawId, 1); - } - - function testCompleteAndStartNextDraw_notManager() public { - vm.prank(address(0)); - vm.expectRevert(abi.encodeWithSelector(CallerNotDrawManager.selector, address(0), address(this))); - prizePool.completeAndStartNextDraw(winningRandomNumber); - } - - function testCompleteAndStartNextDraw_notElapsed_atStart() public { - vm.warp(lastCompletedDrawStartedAt); - vm.expectRevert(abi.encodeWithSelector(DrawNotFinished.selector, lastCompletedDrawStartedAt + drawPeriodSeconds)); - prizePool.completeAndStartNextDraw(winningRandomNumber); - } - - function testCompleteAndStartNextDraw_notElapsed_subsequent() public { - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds); - prizePool.completeAndStartNextDraw(winningRandomNumber); - vm.expectRevert(abi.encodeWithSelector(DrawNotFinished.selector, lastCompletedDrawStartedAt + drawPeriodSeconds * 2)); - prizePool.completeAndStartNextDraw(winningRandomNumber); - } - - function testCompleteAndStartNextDraw_notElapsed_nextDrawPartway() public { - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds); - prizePool.completeAndStartNextDraw(winningRandomNumber); - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds + drawPeriodSeconds / 2); - vm.expectRevert(abi.encodeWithSelector(DrawNotFinished.selector, lastCompletedDrawStartedAt + drawPeriodSeconds * 2)); - prizePool.completeAndStartNextDraw(winningRandomNumber); - } - - function testCompleteAndStartNextDraw_notElapsed_partway() public { - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds / 2); - vm.expectRevert(abi.encodeWithSelector(DrawNotFinished.selector, lastCompletedDrawStartedAt + drawPeriodSeconds)); - prizePool.completeAndStartNextDraw(winningRandomNumber); - } - - function testCompleteAndStartNextDraw_invalidNumber() public { - vm.expectRevert(abi.encodeWithSelector(RandomNumberIsZero.selector)); - prizePool.completeAndStartNextDraw(0); - } - function testCompleteAndStartNextDraw_noLiquidity() public { - completeAndStartNextDraw(winningRandomNumber); - - assertEq(prizePool.getWinningRandomNumber(), winningRandomNumber); - assertEq(prizePool.getLastCompletedDrawId(), 1); - assertEq(prizePool.getNextDrawId(), 2); - assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); - assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.lastCompletedDrawAwardedAt(), block.timestamp); - } - - function testCompleteAndStartNextDraw_withLiquidity() public { - contribute(100e18); - // = 1e18 / 220e18 = 0.004545454... - // but because of alpha only 10% is released on this draw - completeAndStartNextDraw(winningRandomNumber); - assertEq(fromUD34x4(prizePool.prizeTokenPerShare()), 0.045454545454545454e18, "prize token per share"); - assertEq(prizePool.reserve(), 0.45454545454545466e18, "reserve"); // remainder of the complex fraction - assertEq(prizePool.getTotalContributionsForCompletedDraw(), 10e18); // ensure not a single wei is lost! - } - - function testTotalContributionsForCompletedDraw_noClaims() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getTotalContributionsForCompletedDraw(), 10e18, "first draw"); // 10e18 - completeAndStartNextDraw(winningRandomNumber); - // liquidity should carry over! - assertEq(prizePool.getTotalContributionsForCompletedDraw(), 8.999999999999998700e18, "second draw"); // 10e18 + 9e18 - } - - function testCompleteAndStartNextDraw_shrinkTiers() public { - uint8 startingTiers = 5; - - // reset prize pool at higher tiers - ConstructorParams memory prizePoolParams = ConstructorParams( - prizeToken, - twabController, - address(this), - uint16(365), - drawPeriodSeconds, - lastCompletedDrawStartedAt, - startingTiers, // higher number of tiers - 100, - 10, - 10, - ud2x18(0.9e18), // claim threshold of 90% - sd1x18(0.9e18) // alpha - ); - prizePool = new PrizePool(prizePoolParams); - - contribute(420e18); - completeAndStartNextDraw(1234); - - // tier 0 liquidity: 10e18 - // tier 1 liquidity: 10e18 - // tier 2 liquidity: 10e18 - // tier 3 liquidity: 10e18 - // canary liquidity: 1e18 - // reserve liquidity: 1e18 - - // tiers should not change upon first draw - assertEq(prizePool.numberOfTiers(), startingTiers, "starting tiers"); - assertEq(prizePool.reserve(), 1e18, "reserve after first draw"); - - // now claim only grand prize - mockTwab(address(this), 0); - claimPrize(address(this), 0, 0); - - vm.expectEmit(); - // shrink to minimum - emit DrawCompleted(2, 4567, startingTiers, 3); - - completeAndStartNextDraw(4567); - // reclaimed tier 2, 3, and canary. 22e18 in total. - // draw 2 has 37.8. Reserve is 10/220.0 * 37.8e18 = 1.718181818181818e18 - // 22e18 + 1.718181818181818e18 = 23.718181818181818e18 - // shrink by 2 - assertEq(prizePool.numberOfTiers(), 3, "number of tiers"); - assertEq(prizePool.reserve(), 23.71818181818181801e18, "size of reserve"); - } - - function testCompleteAndStartNextDraw_expandingTiers() public { - contribute(1e18); - completeAndStartNextDraw(1234); - mockTwab(address(this), 0); - claimPrize(address(this), 0, 0); - mockTwab(sender1, 1); - claimPrize(sender1, 1, 0); - mockTwab(sender2, 1); - claimPrize(sender2, 1, 0); - mockTwab(sender3, 1); - claimPrize(sender3, 1, 0); - mockTwab(sender4, 1); - claimPrize(sender4, 1, 0); - - // canary tiers - mockTwab(sender5, 2); - claimPrize(sender5, 2, 0); - mockTwab(sender6, 2); - claimPrize(sender6, 2, 0); - - vm.expectEmit(); - emit DrawCompleted(2, 245, 3, 4); - - completeAndStartNextDraw(245); - assertEq(prizePool.numberOfTiers(), 4); - } - - function testCompleteAndStartNextDraw_multipleDraws() public { - contribute(1e18); - completeAndStartNextDraw(1234); - completeAndStartNextDraw(1234); - completeAndStartNextDraw(554); - - mockTwab(sender5, 1); - assertTrue(claimPrize(sender5, 1, 0) > 0, "has prize"); - } - - function testCompleteAndStartNextDraw_emitsEvent() public { - vm.expectEmit(); - emit DrawCompleted(1, 12345, 3, 3); - completeAndStartNextDraw(12345); - } - - function testGetTotalShares() public { - assertEq(prizePool.getTotalShares(), 220); - } - - function testGetRemainingTierLiquidity_invalidTier() public { - assertEq(prizePool.getRemainingTierLiquidity(10), 0); - } - - function testGetRemainingTierLiquidity_grandPrize() public { - contribute(1e18); - completeAndStartNextDraw(winningRandomNumber); - // 2 tiers at 100 shares each, and 10 for canary and 10 for reserve - // = 100 / 220 = 10 / 22 = 0.45454545454545453 - // then take only 10% due to alpha = 0.9 - assertEq(prizePool.getRemainingTierLiquidity(0), 0.0454545454545454e18); - } - - function testGetRemainingTierLiquidity_afterClaim() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - uint256 liquidity = 4.5454545454545454e18; - assertEq(prizePool.getRemainingTierLiquidity(1), liquidity, "second tier"); - - mockTwab(sender1, 1); - uint256 prize = 1.13636363636363635e18; - assertEq(claimPrize(sender1, 1, 0), prize, "second tier prize 1"); - - // reduce by prize - assertEq(prizePool.getRemainingTierLiquidity(1), liquidity - prize, "second tier liquidity post claim 1"); - } - - function testGetRemainingTierLiquidity_canary() public { - contribute(220e18); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getRemainingTierLiquidity(0), 10e18); - assertEq(prizePool.getRemainingTierLiquidity(1), 10e18); - // canary tier - assertEq(prizePool.getRemainingTierLiquidity(2), 1e18); - } - - function testSetDrawManager() public { - params.drawManager = address(0); - prizePool = new PrizePool(params); - vm.expectEmit(); - emit DrawManagerSet(address(this)); - prizePool.setDrawManager(address(this)); - assertEq(prizePool.drawManager(), address(this)); - } - - function testSetDrawManager_alreadySet() public { - vm.expectRevert(abi.encodeWithSelector(DrawManagerAlreadySet.selector)); - prizePool.setDrawManager(address(this)); - } - - function testIsWinner_noDraw() public { - vm.expectRevert(abi.encodeWithSelector(NoCompletedDraw.selector)); - prizePool.isWinner(address(this), msg.sender, 10, 0); - } - - function testIsWinner_invalidTier() public { - completeAndStartNextDraw(winningRandomNumber); - vm.expectRevert(abi.encodeWithSelector(InvalidTier.selector, 10, 3)); - prizePool.isWinner(address(this), msg.sender, 10, 0); - } - - function testIsWinnerDailyPrize() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 1); - assertEq(prizePool.isWinner(address(this), msg.sender, 1, 0), true); - } - - function testIsWinnerGrandPrize() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - assertEq(prizePool.isWinner(address(this), msg.sender, 0, 0), true); - } - - function testIsWinner_emitsInvalidPrizeIndex() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 1); - vm.expectRevert(abi.encodeWithSelector(InvalidPrizeIndex.selector, 4, 4, 1)); - prizePool.isWinner(address(this), msg.sender, 1, 4); - } - - function testWasClaimed_not() public { - assertEq(prizePool.wasClaimed(msg.sender, 0, 0), false); - } - - function testWasClaimed_single() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - assertEq(prizePool.wasClaimed(msg.sender, 0, 0), true); - } - - function testWasClaimed_old_draw() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - assertEq(prizePool.wasClaimed(msg.sender, 0, 0), true); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.wasClaimed(msg.sender, 0, 0), false); - } - - function testClaimPrize_single() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0); - // grand prize is (100/220) * 0.1 * 100e18 = 4.5454...e18 - assertEq(prizeToken.balanceOf(msg.sender), 4.5454545454545454e18); - assertEq(prizePool.claimCount(), 1); - } - - function testClaimPrize_withFee() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - // total prize size is returned - claimPrize(msg.sender, 0, 0, 1e18, address(this)); - // grand prize is (100/220) * 0.1 * 100e18 = 4.5454...e18 - assertEq(prizeToken.balanceOf(msg.sender), 3.5454545454545454e18, "user balance after claim"); - assertEq(prizePool.claimCount(), 1); - assertEq(prizePool.balanceOfClaimRewards(address(this)), 1e18); - } - - function testClaimPrize_notWinner() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - vm.expectRevert(abi.encodeWithSelector(DidNotWin.selector, msg.sender, address(this), 0, 0)); - claimPrize(msg.sender, 0, 0); - } - - function testClaimPrize_feeTooLarge() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - vm.expectRevert(abi.encodeWithSelector(FeeTooLarge.selector, 10e18, 4.5454545454545454e18)); - claimPrize(msg.sender, 0, 0, 10e18, address(0)); - } - - function testClaimPrize_grandPrize_claimTwice() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - assertEq(claimPrize(msg.sender, 0, 0), 4.5454545454545454e18, "prize size"); - // second claim is zero - vm.expectRevert(abi.encodeWithSelector(AlreadyClaimedPrize.selector, msg.sender, 0)); - claimPrize(msg.sender, 0, 0); - } - - function testClaimPrize_secondTier_claimTwice() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 1); - assertEq(claimPrize(msg.sender, 1, 0), 1.13636363636363635e18, "first claim"); - // second claim is same - mockTwab(sender2, 1); - assertEq(claimPrize(sender2, 1, 0), 1.13636363636363635e18, "second claim"); - } - - function testClaimCanaryPrize() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(sender1, 2); - claimPrize(sender1, 2, 0); - assertEq(prizePool.claimCount(), 0); - assertEq(prizePool.canaryClaimCount(), 1); - } - - function testClaimPrizePartial() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(sender1, 2); - claimPrize(sender1, 2, 0); - assertEq(prizePool.claimCount(), 0); - assertEq(prizePool.canaryClaimCount(), 1); - } - - function testTotalClaimedPrizes() public { - assertEq(prizePool.totalWithdrawn(), 0); - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - uint256 prize = 4.5454545454545454e18; - assertEq(claimPrize(msg.sender, 0, 0), prize, "prize size"); - assertEq(prizePool.totalWithdrawn(), prize, "total claimed prize"); - } - - function testLastCompletedDrawStartedAt() public { - assertEq(prizePool.lastCompletedDrawStartedAt(), 0); - completeAndStartNextDraw(winningRandomNumber); - - assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); - assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.lastCompletedDrawAwardedAt(), block.timestamp); - } - - function testLastCompletedDrawEndedAt() public { - assertEq(prizePool.lastCompletedDrawEndedAt(), 0); - completeAndStartNextDraw(winningRandomNumber); - - assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); - assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.lastCompletedDrawAwardedAt(), block.timestamp); - } - - function testLastCompletedDrawAwardedAt() public { - assertEq(prizePool.lastCompletedDrawAwardedAt(), 0); - - uint64 targetTimestamp = prizePool.nextDrawEndsAt() + 3 hours; - - vm.warp(targetTimestamp); - prizePool.completeAndStartNextDraw(winningRandomNumber); - - assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); - assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.lastCompletedDrawAwardedAt(), targetTimestamp); - } - - function testHasNextDrawFinished() public { - assertEq(prizePool.hasNextDrawFinished(), false); - vm.warp(prizePool.nextDrawEndsAt() - 1); - assertEq(prizePool.hasNextDrawFinished(), false); - vm.warp(prizePool.nextDrawEndsAt()); - assertEq(prizePool.hasNextDrawFinished(), true); - } - - function testWithdrawClaimRewards_sufficient() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - claimPrize(msg.sender, 0, 0, 1e18, address(this)); - prizePool.withdrawClaimRewards(address(this), 1e18); - assertEq(prizeToken.balanceOf(address(this)), 1e18); - } - - function testWithdrawClaimRewards_insufficient() public { - vm.expectRevert(abi.encodeWithSelector(InsufficientRewardsError.selector, 1e18, 0)); - prizePool.withdrawClaimRewards(address(this), 1e18); - } - - function testWithdrawClaimRewards_emitsEvent() public { - contribute(100e18); - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, 0); - - address[] memory winners = new address[](1); - winners[0] = msg.sender; - uint32[][] memory prizeIndices = new uint32[][](1); - prizeIndices[0] = new uint32[](1); - prizeIndices[0][0] = 0; - prizePool.claimPrizes(0, winners, prizeIndices, 1e18, address(this)); - - vm.expectEmit(); - emit WithdrawClaimRewards(address(this), 5e17, 1e18); - prizePool.withdrawClaimRewards(address(this), 5e17); - } - - function testClaimPrizes_winnerPrizeMismatch() public { - vm.expectRevert(abi.encodeWithSelector(WinnerPrizeMismatch.selector, 2, 3)); - address[] memory winners = new address[](2); - uint32[][] memory prizeIndices = new uint32[][](3); - prizePool.claimPrizes(0, winners, prizeIndices, 0, address(this)); - } - - function testNextDrawStartsAt_zeroDraw() public { - // current time *is* lastCompletedDrawStartedAt - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); - } - - function testNextDrawStartsAt_zeroDrawPartwayThrough() public { - // current time is halfway through first draw - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds / 2); - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); - } - - function testNextDrawStartsAt_zeroDrawWithLongDelay() public { - // current time is halfway through *second* draw - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds + drawPeriodSeconds / 2); // warp halfway through second draw - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); - } - - function testNextDrawStartsAt_nextDraw() public { - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt+drawPeriodSeconds); - } - - function testNextDrawIncludesMissedDraws() public { - assertEq(prizePool.getNextDrawId(), 1); - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.nextDrawEndsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getNextDrawId(), 2); - } - - function testNextDrawIncludesMissedDraws_middleOfDraw() public { - assertEq(prizePool.getNextDrawId(), 1); - vm.warp(lastCompletedDrawStartedAt + (drawPeriodSeconds * 5) / 2); - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.nextDrawEndsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getNextDrawId(), 2); - } - - function testNextDrawIncludesMissedDraws_2Draws() public { - assertEq(prizePool.getNextDrawId(), 1); - vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds * 3); - assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - assertEq(prizePool.nextDrawEndsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 3); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getNextDrawId(), 2); - } - - function testNextDrawIncludesMissedDraws_notFirstDraw() public { - completeAndStartNextDraw(winningRandomNumber); - uint64 _lastCompletedDrawStartedAt = prizePool.lastCompletedDrawStartedAt(); - assertEq(prizePool.getNextDrawId(), 2); - vm.warp(_lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - assertEq(prizePool.nextDrawStartsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.nextDrawEndsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getNextDrawId(), 3); - } - - function testNextDrawIncludesMissedDraws_manyDrawsIn_manyMissed() public { - completeAndStartNextDraw(winningRandomNumber); - completeAndStartNextDraw(winningRandomNumber); - completeAndStartNextDraw(winningRandomNumber); - completeAndStartNextDraw(winningRandomNumber); - uint64 _lastCompletedDrawStartedAt = prizePool.lastCompletedDrawStartedAt(); - assertEq(prizePool.getNextDrawId(), 5); - vm.warp(_lastCompletedDrawStartedAt + drawPeriodSeconds * 5); - assertEq(prizePool.nextDrawStartsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 4); - assertEq(prizePool.nextDrawEndsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 5); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getNextDrawId(), 6); - } - - function testNextDrawIncludesMissedDraws_notFirstDraw_middleOfDraw() public { - completeAndStartNextDraw(winningRandomNumber); - uint64 _lastCompletedDrawStartedAt = prizePool.lastCompletedDrawStartedAt(); - assertEq(prizePool.getNextDrawId(), 2); - vm.warp(_lastCompletedDrawStartedAt + (drawPeriodSeconds * 5) / 2); - assertEq(prizePool.nextDrawStartsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds); - assertEq(prizePool.nextDrawEndsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 2); - completeAndStartNextDraw(winningRandomNumber); - assertEq(prizePool.getNextDrawId(), 3); - } - - function testGetVaultUserBalanceAndTotalSupplyTwab() public { - completeAndStartNextDraw(winningRandomNumber); - mockTwab(msg.sender, prizePool.lastCompletedDrawEndedAt() - 365 * drawPeriodSeconds, prizePool.lastCompletedDrawEndedAt()); - (uint256 twab, uint256 twabTotalSupply) = prizePool.getVaultUserBalanceAndTotalSupplyTwab(address(this), msg.sender, 365); - assertEq(twab, 366e30); - assertEq(twabTotalSupply, 1e30); - } - - function mockGetAverageBalanceBetween(address _vault, address _user, uint64 _startTime, uint64 _endTime, uint256 _result) internal { - vm.mockCall( - address(twabController), - abi.encodeWithSelector(TwabController.getTwabBetween.selector, _vault, _user, _startTime, _endTime), - abi.encode(_result) - ); - } - - function mockGetAverageTotalSupplyBetween(address _vault, uint32 _startTime, uint32 _endTime, uint256 _result) internal { - vm.mockCall( - address(twabController), - abi.encodeWithSelector(TwabController.getTotalSupplyTwabBetween.selector, _vault, _startTime, _endTime), - abi.encode(_result) - ); - } - - function testEstimatedPrizeCount_current() public { - assertEq(prizePool.estimatedPrizeCount(), 4); - } - - function testEstimatedPrizeCount() public { - // assumes grand prize is 365 - assertEq(prizePool.estimatedPrizeCount(0), 0); - assertEq(prizePool.estimatedPrizeCount(1), 0); - assertEq(prizePool.estimatedPrizeCount(2), 0); - assertEq(prizePool.estimatedPrizeCount(3), 4); - assertEq(prizePool.estimatedPrizeCount(4), 16); - assertEq(prizePool.estimatedPrizeCount(5), 66); - assertEq(prizePool.estimatedPrizeCount(6), 270); - assertEq(prizePool.estimatedPrizeCount(7), 1108); - assertEq(prizePool.estimatedPrizeCount(8), 4517); - assertEq(prizePool.estimatedPrizeCount(9), 18358); - assertEq(prizePool.estimatedPrizeCount(10), 74435); - assertEq(prizePool.estimatedPrizeCount(11), 301239); - assertEq(prizePool.estimatedPrizeCount(12), 1217266); - assertEq(prizePool.estimatedPrizeCount(13), 4912619); - assertEq(prizePool.estimatedPrizeCount(14), 19805536); - assertEq(prizePool.estimatedPrizeCount(15), 79777187); - assertEq(prizePool.estimatedPrizeCount(16), 0); - } - - function testcanaryPrizeCountFractional() public { - // assuming 10 reserve, 10 canary, and 100 per tier - assertEq(prizePool.canaryPrizeCountFractional(0).unwrap(), 0); - assertEq(prizePool.canaryPrizeCountFractional(1).unwrap(), 0); - assertEq(prizePool.canaryPrizeCountFractional(2).unwrap(), 0); - assertEq(prizePool.canaryPrizeCountFractional(3).unwrap(), 2327272727272727264); - assertEq(prizePool.canaryPrizeCountFractional(4).unwrap(), 8400000000000000000); - assertEq(prizePool.canaryPrizeCountFractional(5).unwrap(), 31695238095238095104); - assertEq(prizePool.canaryPrizeCountFractional(6).unwrap(), 122092307692307691520); - assertEq(prizePool.canaryPrizeCountFractional(7).unwrap(), 475664516129032257536); - assertEq(prizePool.canaryPrizeCountFractional(8).unwrap(), 1865955555555555540992); - assertEq(prizePool.canaryPrizeCountFractional(9).unwrap(), 7352819512195121938432); - assertEq(prizePool.canaryPrizeCountFractional(10).unwrap(), 29063791304347825995776); - assertEq(prizePool.canaryPrizeCountFractional(11).unwrap(), 115137756862745097011200); - assertEq(prizePool.canaryPrizeCountFractional(12).unwrap(), 456879542857142854746112); - assertEq(prizePool.canaryPrizeCountFractional(13).unwrap(), 1815239763934426215481344); - assertEq(prizePool.canaryPrizeCountFractional(14).unwrap(), 7219286884848484797644800); - assertEq(prizePool.canaryPrizeCountFractional(15).unwrap(), 28733936135211267454402560); - assertEq(prizePool.canaryPrizeCountFractional(16).unwrap(), 0); - } - - function contribute(uint256 amountContributed) public { - contribute(amountContributed, address(this)); - } - - function contribute(uint256 amountContributed, address to) public { - prizeToken.mint(address(prizePool), amountContributed); - prizePool.contributePrizeTokens(to, amountContributed); - } - - function completeAndStartNextDraw(uint256 _winningRandomNumber) public { - vm.warp(prizePool.nextDrawEndsAt()); - prizePool.completeAndStartNextDraw(_winningRandomNumber); - } - - function claimPrize(address sender, uint8 tier, uint32 prizeIndex) public returns (uint256) { - return claimPrize(sender, tier, prizeIndex, 0, address(0)); - } - - function claimPrize(address sender, uint8 tier, uint32 prizeIndex, uint96 fee, address feeRecipient) public returns (uint256) { - address[] memory winners = new address[](1); - winners[0] = sender; - uint32[][] memory prizeIndices = new uint32[][](1); - prizeIndices[0] = new uint32[](1); - prizeIndices[0][0] = prizeIndex; - return prizePool.claimPrizes(tier, winners, prizeIndices, fee, feeRecipient); - } - - function mockTwab(address _account, uint256 startTime, uint256 endTime) public { - // console2.log("mockTwab startTime", startTime); - // console2.log("mockTwab endTime", endTime); - mockGetAverageBalanceBetween( - address(this), - _account, - uint32(startTime), - uint32(endTime), - 366e30 - ); - mockGetAverageTotalSupplyBetween( - address(this), - uint32(startTime), - uint32(endTime), - 1e30 - ); - } - - function mockTwab(address _account, uint8 _tier) public { - (uint64 startTime, uint64 endTime) = prizePool.calculateTierTwabTimestamps(_tier); - mockTwab(_account, startTime, endTime); - } + assertEq(prizePool.reserveForNextDraw(), 1.318181818181818310e18); + } + + function testWithdrawReserve_notManager() public { + vm.prank(address(0)); + vm.expectRevert( + abi.encodeWithSelector(CallerNotDrawManager.selector, address(0), address(this)) + ); + prizePool.withdrawReserve(address(0), 1); + } + + function testWithdrawReserve_insuff() public { + vm.expectRevert(abi.encodeWithSelector(InsufficientReserve.selector, 1, 0)); + prizePool.withdrawReserve(address(this), 1); + } + + function testWithdrawReserve() public { + contribute(220e18); + completeAndStartNextDraw(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); + } + + function testCanaryPrizeCount_noParam() public { + assertEq(prizePool.canaryPrizeCount(), 2); + } + + function testCanaryPrizeCount_param() public { + assertEq(prizePool.canaryPrizeCount(4), 8); + } + + function testGetTotalContributedBetween() public { + contribute(10e18); + assertEq(prizePool.getTotalContributedBetween(1, 1), 1e18); + } + + function testGetContributedBetween() public { + contribute(10e18); + assertEq(prizePool.getContributedBetween(address(this), 1, 1), 1e18); + } + + function testGetTierAccrualDurationInDraws() public { + assertEq(prizePool.getTierAccrualDurationInDraws(0), 366); + } + + function testGetTierPrizeCount() public { + assertEq(prizePool.getTierPrizeCount(3), 4 ** 3); + } + + function testContributePrizeTokens() public { + contribute(100); + assertEq(prizeToken.balanceOf(address(prizePool)), 100); + } + + function testContributePrizeTokens_emitsEvent() public { + prizeToken.mint(address(prizePool), 100); + vm.expectEmit(); + emit ContributePrizeTokens(address(this), 1, 100); + prizePool.contributePrizeTokens(address(this), 100); + } + + function testContributePrizeTokens_emitsContributionGTDeltaBalance() public { + vm.expectRevert(abi.encodeWithSelector(ContributionGTDeltaBalance.selector, 100, 0)); + prizePool.contributePrizeTokens(address(this), 100); + } + + function testAccountedBalance_withdrawnReserve() public { + contribute(100e18); + completeAndStartNextDraw(1); + assertEq(prizePool.reserve(), 0.45454545454545466e18); + prizePool.withdrawReserve(address(this), uint104(prizePool.reserve())); + assertEq(prizePool.accountedBalance(), prizeToken.balanceOf(address(prizePool))); + assertEq(prizePool.reserve(), 0); + } + + function testAccountedBalance_noClaims() public { + contribute(100); + assertEq(prizePool.accountedBalance(), 100); + } + + function testAccountedBalance_oneClaim() public { + contribute(100e18); + completeAndStartNextDraw(1); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + assertEq(prizePool.accountedBalance(), 95.4545454545454546e18); + } + + function testAccountedBalance_oneClaim_andMoreContrib() public { + contribute(100e18); + completeAndStartNextDraw(1); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + contribute(10e18); + assertEq(prizePool.accountedBalance(), 105.4545454545454546e18); + } + + function testAccountedBalance_twoClaims() public { + contribute(100e18); + completeAndStartNextDraw(1); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + // 10e18 - 4.5454545454545454e18 = 5.4545454545454546e18 + completeAndStartNextDraw(1); + + // 9e18*100/220 = 4.0909090909090909e18 + assertEq(prizePool.accountedBalance(), 95.4545454545454546e18, "accounted balance"); + + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + } + + function testGetVaultPortion_WhenEmpty() public { + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 0)), 0); + } + + function testGetVaultPortion_WhenOne() public { + contribute(100e18); // available draw 1 + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 1, 1)), 1e18); + } + + function testGetVaultPortion_WhenTwo() public { + contribute(100e18); // available draw 1 + contribute(100e18, address(sender1)); // available draw 1 + + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 1, 1)), 0.5e18); + } + + function testGetVaultPortion_WhenTwo_AccrossTwoDraws() public { + contribute(100e18); + contribute(100e18, address(sender1)); + + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 1, 2)), 0.5e18); + } + + function testGetVaultPortion_BeforeContribution() public { + contribute(100e18); // available on draw 1 + + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 0)), 0); + } + + function testGetVaultPortion_BeforeContributionOnDraw3() public { + completeAndStartNextDraw(winningRandomNumber); // draw 1 + completeAndStartNextDraw(winningRandomNumber); // draw 2 + assertEq(prizePool.getLastCompletedDrawId(), 2); + contribute(100e18); // available on draw 3 + + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 2, 2)), 0); + } + + function testGetVaultPortion_BeforeAndAtContribution() public { + contribute(100e18); // available draw 1 + + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 1)), 1e18); + } + + function testGetVaultPortion_BeforeAndAfterContribution() public { + completeAndStartNextDraw(winningRandomNumber); // draw 1 + contribute(100e18); // available draw 2 + + assertEq(SD59x18.unwrap(prizePool.getVaultPortion(address(this), 0, 2)), 1e18); + } + + function testGetNextDrawId() public { + uint256 nextDrawId = prizePool.getNextDrawId(); + assertEq(nextDrawId, 1); + } + + function testCompleteAndStartNextDraw_notManager() public { + vm.prank(address(0)); + vm.expectRevert( + abi.encodeWithSelector(CallerNotDrawManager.selector, address(0), address(this)) + ); + prizePool.completeAndStartNextDraw(winningRandomNumber); + } + + function testCompleteAndStartNextDraw_notElapsed_atStart() public { + vm.warp(lastCompletedDrawStartedAt); + vm.expectRevert( + abi.encodeWithSelector( + DrawNotFinished.selector, + lastCompletedDrawStartedAt + drawPeriodSeconds + ) + ); + prizePool.completeAndStartNextDraw(winningRandomNumber); + } + + function testCompleteAndStartNextDraw_notElapsed_subsequent() public { + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds); + prizePool.completeAndStartNextDraw(winningRandomNumber); + vm.expectRevert( + abi.encodeWithSelector( + DrawNotFinished.selector, + lastCompletedDrawStartedAt + drawPeriodSeconds * 2 + ) + ); + prizePool.completeAndStartNextDraw(winningRandomNumber); + } + + function testCompleteAndStartNextDraw_notElapsed_nextDrawPartway() public { + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds); + prizePool.completeAndStartNextDraw(winningRandomNumber); + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds + drawPeriodSeconds / 2); + vm.expectRevert( + abi.encodeWithSelector( + DrawNotFinished.selector, + lastCompletedDrawStartedAt + drawPeriodSeconds * 2 + ) + ); + prizePool.completeAndStartNextDraw(winningRandomNumber); + } + + function testCompleteAndStartNextDraw_notElapsed_partway() public { + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds / 2); + vm.expectRevert( + abi.encodeWithSelector( + DrawNotFinished.selector, + lastCompletedDrawStartedAt + drawPeriodSeconds + ) + ); + prizePool.completeAndStartNextDraw(winningRandomNumber); + } + + function testCompleteAndStartNextDraw_invalidNumber() public { + vm.expectRevert(abi.encodeWithSelector(RandomNumberIsZero.selector)); + prizePool.completeAndStartNextDraw(0); + } + + function testCompleteAndStartNextDraw_noLiquidity() public { + completeAndStartNextDraw(winningRandomNumber); + + assertEq(prizePool.getWinningRandomNumber(), winningRandomNumber); + assertEq(prizePool.getLastCompletedDrawId(), 1); + assertEq(prizePool.getNextDrawId(), 2); + assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); + assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.lastCompletedDrawAwardedAt(), block.timestamp); + } + + function testCompleteAndStartNextDraw_withLiquidity() public { + contribute(100e18); + // = 1e18 / 220e18 = 0.004545454... + // but because of alpha only 10% is released on this draw + completeAndStartNextDraw(winningRandomNumber); + assertEq( + fromUD34x4(prizePool.prizeTokenPerShare()), + 0.045454545454545454e18, + "prize token per share" + ); + assertEq(prizePool.reserve(), 0.45454545454545466e18, "reserve"); // remainder of the complex fraction + assertEq(prizePool.getTotalContributionsForCompletedDraw(), 10e18); // ensure not a single wei is lost! + } + + function testTotalContributionsForCompletedDraw_noClaims() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getTotalContributionsForCompletedDraw(), 10e18, "first draw"); // 10e18 + completeAndStartNextDraw(winningRandomNumber); + // liquidity should carry over! + assertEq( + prizePool.getTotalContributionsForCompletedDraw(), + 8.999999999999998700e18, + "second draw" + ); // 10e18 + 9e18 + } + + function testCompleteAndStartNextDraw_shrinkTiers() public { + uint8 startingTiers = 5; + + // reset prize pool at higher tiers + ConstructorParams memory prizePoolParams = ConstructorParams( + prizeToken, + twabController, + address(this), + uint16(365), + drawPeriodSeconds, + lastCompletedDrawStartedAt, + startingTiers, // higher number of tiers + 100, + 10, + 10, + ud2x18(0.9e18), // claim threshold of 90% + sd1x18(0.9e18) // alpha + ); + prizePool = new PrizePool(prizePoolParams); + + contribute(420e18); + completeAndStartNextDraw(1234); + + // tier 0 liquidity: 10e18 + // tier 1 liquidity: 10e18 + // tier 2 liquidity: 10e18 + // tier 3 liquidity: 10e18 + // canary liquidity: 1e18 + // reserve liquidity: 1e18 + + // tiers should not change upon first draw + assertEq(prizePool.numberOfTiers(), startingTiers, "starting tiers"); + assertEq(prizePool.reserve(), 1e18, "reserve after first draw"); + + // now claim only grand prize + mockTwab(address(this), 0); + claimPrize(address(this), 0, 0); + + vm.expectEmit(); + // shrink to minimum + emit DrawCompleted(2, 4567, startingTiers, 3); + + completeAndStartNextDraw(4567); + // reclaimed tier 2, 3, and canary. 22e18 in total. + // draw 2 has 37.8. Reserve is 10/220.0 * 37.8e18 = 1.718181818181818e18 + // 22e18 + 1.718181818181818e18 = 23.718181818181818e18 + // shrink by 2 + assertEq(prizePool.numberOfTiers(), 3, "number of tiers"); + assertEq(prizePool.reserve(), 23.71818181818181801e18, "size of reserve"); + } + + function testCompleteAndStartNextDraw_expandingTiers() public { + contribute(1e18); + completeAndStartNextDraw(1234); + mockTwab(address(this), 0); + claimPrize(address(this), 0, 0); + mockTwab(sender1, 1); + claimPrize(sender1, 1, 0); + mockTwab(sender2, 1); + claimPrize(sender2, 1, 0); + mockTwab(sender3, 1); + claimPrize(sender3, 1, 0); + mockTwab(sender4, 1); + claimPrize(sender4, 1, 0); + + // canary tiers + mockTwab(sender5, 2); + claimPrize(sender5, 2, 0); + mockTwab(sender6, 2); + claimPrize(sender6, 2, 0); + + vm.expectEmit(); + emit DrawCompleted(2, 245, 3, 4); + + completeAndStartNextDraw(245); + assertEq(prizePool.numberOfTiers(), 4); + } + + function testCompleteAndStartNextDraw_multipleDraws() public { + contribute(1e18); + completeAndStartNextDraw(1234); + completeAndStartNextDraw(1234); + completeAndStartNextDraw(554); + + mockTwab(sender5, 1); + assertTrue(claimPrize(sender5, 1, 0) > 0, "has prize"); + } + + function testCompleteAndStartNextDraw_emitsEvent() public { + vm.expectEmit(); + emit DrawCompleted(1, 12345, 3, 3); + completeAndStartNextDraw(12345); + } + + function testGetTotalShares() public { + assertEq(prizePool.getTotalShares(), 220); + } + + function testGetRemainingTierLiquidity_invalidTier() public { + assertEq(prizePool.getRemainingTierLiquidity(10), 0); + } + + function testGetRemainingTierLiquidity_grandPrize() public { + contribute(1e18); + completeAndStartNextDraw(winningRandomNumber); + // 2 tiers at 100 shares each, and 10 for canary and 10 for reserve + // = 100 / 220 = 10 / 22 = 0.45454545454545453 + // then take only 10% due to alpha = 0.9 + assertEq(prizePool.getRemainingTierLiquidity(0), 0.0454545454545454e18); + } + + function testGetRemainingTierLiquidity_afterClaim() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + uint256 liquidity = 4.5454545454545454e18; + assertEq(prizePool.getRemainingTierLiquidity(1), liquidity, "second tier"); + + mockTwab(sender1, 1); + uint256 prize = 1.13636363636363635e18; + assertEq(claimPrize(sender1, 1, 0), prize, "second tier prize 1"); + + // reduce by prize + assertEq( + prizePool.getRemainingTierLiquidity(1), + liquidity - prize, + "second tier liquidity post claim 1" + ); + } + + function testGetRemainingTierLiquidity_canary() public { + contribute(220e18); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getRemainingTierLiquidity(0), 10e18); + assertEq(prizePool.getRemainingTierLiquidity(1), 10e18); + // canary tier + assertEq(prizePool.getRemainingTierLiquidity(2), 1e18); + } + + function testSetDrawManager() public { + params.drawManager = address(0); + prizePool = new PrizePool(params); + vm.expectEmit(); + emit DrawManagerSet(address(this)); + prizePool.setDrawManager(address(this)); + assertEq(prizePool.drawManager(), address(this)); + } + + function testSetDrawManager_alreadySet() public { + vm.expectRevert(abi.encodeWithSelector(DrawManagerAlreadySet.selector)); + prizePool.setDrawManager(address(this)); + } + + function testIsWinner_noDraw() public { + vm.expectRevert(abi.encodeWithSelector(NoCompletedDraw.selector)); + prizePool.isWinner(address(this), msg.sender, 10, 0); + } + + function testIsWinner_invalidTier() public { + completeAndStartNextDraw(winningRandomNumber); + vm.expectRevert(abi.encodeWithSelector(InvalidTier.selector, 10, 3)); + prizePool.isWinner(address(this), msg.sender, 10, 0); + } + + function testIsWinnerDailyPrize() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 1); + assertEq(prizePool.isWinner(address(this), msg.sender, 1, 0), true); + } + + function testIsWinnerGrandPrize() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + assertEq(prizePool.isWinner(address(this), msg.sender, 0, 0), true); + } + + function testIsWinner_emitsInvalidPrizeIndex() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 1); + vm.expectRevert(abi.encodeWithSelector(InvalidPrizeIndex.selector, 4, 4, 1)); + prizePool.isWinner(address(this), msg.sender, 1, 4); + } + + function testWasClaimed_not() public { + assertEq(prizePool.wasClaimed(msg.sender, 0, 0), false); + } + + function testWasClaimed_single() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + assertEq(prizePool.wasClaimed(msg.sender, 0, 0), true); + } + + function testWasClaimed_old_draw() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + assertEq(prizePool.wasClaimed(msg.sender, 0, 0), true); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.wasClaimed(msg.sender, 0, 0), false); + } + + function testClaimPrize_single() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0); + // grand prize is (100/220) * 0.1 * 100e18 = 4.5454...e18 + assertEq(prizeToken.balanceOf(msg.sender), 4.5454545454545454e18); + assertEq(prizePool.claimCount(), 1); + } + + function testClaimPrize_withFee() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + // total prize size is returned + claimPrize(msg.sender, 0, 0, 1e18, address(this)); + // grand prize is (100/220) * 0.1 * 100e18 = 4.5454...e18 + assertEq(prizeToken.balanceOf(msg.sender), 3.5454545454545454e18, "user balance after claim"); + assertEq(prizePool.claimCount(), 1); + assertEq(prizePool.balanceOfClaimRewards(address(this)), 1e18); + } + + function testClaimPrize_notWinner() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + vm.expectRevert(abi.encodeWithSelector(DidNotWin.selector, msg.sender, address(this), 0, 0)); + claimPrize(msg.sender, 0, 0); + } + + function testClaimPrize_feeTooLarge() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + vm.expectRevert(abi.encodeWithSelector(FeeTooLarge.selector, 10e18, 4.5454545454545454e18)); + claimPrize(msg.sender, 0, 0, 10e18, address(0)); + } + + function testClaimPrize_grandPrize_claimTwice() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + assertEq(claimPrize(msg.sender, 0, 0), 4.5454545454545454e18, "prize size"); + // second claim is zero + vm.expectRevert(abi.encodeWithSelector(AlreadyClaimedPrize.selector, msg.sender, 0)); + claimPrize(msg.sender, 0, 0); + } + + function testClaimPrize_secondTier_claimTwice() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 1); + assertEq(claimPrize(msg.sender, 1, 0), 1.13636363636363635e18, "first claim"); + // second claim is same + mockTwab(sender2, 1); + assertEq(claimPrize(sender2, 1, 0), 1.13636363636363635e18, "second claim"); + } + + function testClaimCanaryPrize() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(sender1, 2); + claimPrize(sender1, 2, 0); + assertEq(prizePool.claimCount(), 0); + assertEq(prizePool.canaryClaimCount(), 1); + } + + function testClaimPrizePartial() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(sender1, 2); + claimPrize(sender1, 2, 0); + assertEq(prizePool.claimCount(), 0); + assertEq(prizePool.canaryClaimCount(), 1); + } + + function testTotalClaimedPrizes() public { + assertEq(prizePool.totalWithdrawn(), 0); + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + uint256 prize = 4.5454545454545454e18; + assertEq(claimPrize(msg.sender, 0, 0), prize, "prize size"); + assertEq(prizePool.totalWithdrawn(), prize, "total claimed prize"); + } + + function testLastCompletedDrawStartedAt() public { + assertEq(prizePool.lastCompletedDrawStartedAt(), 0); + completeAndStartNextDraw(winningRandomNumber); + + assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); + assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.lastCompletedDrawAwardedAt(), block.timestamp); + } + + function testLastCompletedDrawEndedAt() public { + assertEq(prizePool.lastCompletedDrawEndedAt(), 0); + completeAndStartNextDraw(winningRandomNumber); + + assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); + assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.lastCompletedDrawAwardedAt(), block.timestamp); + } + + function testLastCompletedDrawAwardedAt() public { + assertEq(prizePool.lastCompletedDrawAwardedAt(), 0); + + uint64 targetTimestamp = prizePool.nextDrawEndsAt() + 3 hours; + + vm.warp(targetTimestamp); + prizePool.completeAndStartNextDraw(winningRandomNumber); + + assertEq(prizePool.lastCompletedDrawStartedAt(), lastCompletedDrawStartedAt); + assertEq(prizePool.lastCompletedDrawEndedAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.lastCompletedDrawAwardedAt(), targetTimestamp); + } + + function testHasNextDrawFinished() public { + assertEq(prizePool.hasNextDrawFinished(), false); + vm.warp(prizePool.nextDrawEndsAt() - 1); + assertEq(prizePool.hasNextDrawFinished(), false); + vm.warp(prizePool.nextDrawEndsAt()); + assertEq(prizePool.hasNextDrawFinished(), true); + } + + function testWithdrawClaimRewards_sufficient() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + claimPrize(msg.sender, 0, 0, 1e18, address(this)); + prizePool.withdrawClaimRewards(address(this), 1e18); + assertEq(prizeToken.balanceOf(address(this)), 1e18); + } + + function testWithdrawClaimRewards_insufficient() public { + vm.expectRevert(abi.encodeWithSelector(InsufficientRewardsError.selector, 1e18, 0)); + prizePool.withdrawClaimRewards(address(this), 1e18); + } + + function testWithdrawClaimRewards_emitsEvent() public { + contribute(100e18); + completeAndStartNextDraw(winningRandomNumber); + mockTwab(msg.sender, 0); + + address[] memory winners = new address[](1); + winners[0] = msg.sender; + uint32[][] memory prizeIndices = new uint32[][](1); + prizeIndices[0] = new uint32[](1); + prizeIndices[0][0] = 0; + prizePool.claimPrizes(0, winners, prizeIndices, 1e18, address(this)); + + vm.expectEmit(); + emit WithdrawClaimRewards(address(this), 5e17, 1e18); + prizePool.withdrawClaimRewards(address(this), 5e17); + } + + function testClaimPrizes_winnerPrizeMismatch() public { + vm.expectRevert(abi.encodeWithSelector(WinnerPrizeMismatch.selector, 2, 3)); + address[] memory winners = new address[](2); + uint32[][] memory prizeIndices = new uint32[][](3); + prizePool.claimPrizes(0, winners, prizeIndices, 0, address(this)); + } + + function testNextDrawStartsAt_zeroDraw() public { + // current time *is* lastCompletedDrawStartedAt + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); + } + + function testNextDrawStartsAt_zeroDrawPartwayThrough() public { + // current time is halfway through first draw + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds / 2); + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); + } + + function testNextDrawStartsAt_zeroDrawWithLongDelay() public { + // current time is halfway through *second* draw + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds + drawPeriodSeconds / 2); // warp halfway through second draw + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); + } + + function testNextDrawStartsAt_nextDraw() public { + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + } + + function testNextDrawIncludesMissedDraws() public { + assertEq(prizePool.getNextDrawId(), 1); + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.nextDrawEndsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getNextDrawId(), 2); + } + + function testNextDrawIncludesMissedDraws_middleOfDraw() public { + assertEq(prizePool.getNextDrawId(), 1); + vm.warp(lastCompletedDrawStartedAt + (drawPeriodSeconds * 5) / 2); + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.nextDrawEndsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getNextDrawId(), 2); + } + + function testNextDrawIncludesMissedDraws_2Draws() public { + assertEq(prizePool.getNextDrawId(), 1); + vm.warp(lastCompletedDrawStartedAt + drawPeriodSeconds * 3); + assertEq(prizePool.nextDrawStartsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + assertEq(prizePool.nextDrawEndsAt(), lastCompletedDrawStartedAt + drawPeriodSeconds * 3); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getNextDrawId(), 2); + } + + function testNextDrawIncludesMissedDraws_notFirstDraw() public { + completeAndStartNextDraw(winningRandomNumber); + uint64 _lastCompletedDrawStartedAt = prizePool.lastCompletedDrawStartedAt(); + assertEq(prizePool.getNextDrawId(), 2); + vm.warp(_lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + assertEq(prizePool.nextDrawStartsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.nextDrawEndsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getNextDrawId(), 3); + } + + function testNextDrawIncludesMissedDraws_manyDrawsIn_manyMissed() public { + completeAndStartNextDraw(winningRandomNumber); + completeAndStartNextDraw(winningRandomNumber); + completeAndStartNextDraw(winningRandomNumber); + completeAndStartNextDraw(winningRandomNumber); + uint64 _lastCompletedDrawStartedAt = prizePool.lastCompletedDrawStartedAt(); + assertEq(prizePool.getNextDrawId(), 5); + vm.warp(_lastCompletedDrawStartedAt + drawPeriodSeconds * 5); + assertEq(prizePool.nextDrawStartsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 4); + assertEq(prizePool.nextDrawEndsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 5); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getNextDrawId(), 6); + } + + function testNextDrawIncludesMissedDraws_notFirstDraw_middleOfDraw() public { + completeAndStartNextDraw(winningRandomNumber); + uint64 _lastCompletedDrawStartedAt = prizePool.lastCompletedDrawStartedAt(); + assertEq(prizePool.getNextDrawId(), 2); + vm.warp(_lastCompletedDrawStartedAt + (drawPeriodSeconds * 5) / 2); + assertEq(prizePool.nextDrawStartsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds); + assertEq(prizePool.nextDrawEndsAt(), _lastCompletedDrawStartedAt + drawPeriodSeconds * 2); + completeAndStartNextDraw(winningRandomNumber); + assertEq(prizePool.getNextDrawId(), 3); + } + + function testGetVaultUserBalanceAndTotalSupplyTwab() public { + completeAndStartNextDraw(winningRandomNumber); + mockTwab( + msg.sender, + prizePool.lastCompletedDrawEndedAt() - 365 * drawPeriodSeconds, + prizePool.lastCompletedDrawEndedAt() + ); + (uint256 twab, uint256 twabTotalSupply) = prizePool.getVaultUserBalanceAndTotalSupplyTwab( + address(this), + msg.sender, + 365 + ); + assertEq(twab, 366e30); + assertEq(twabTotalSupply, 1e30); + } + + function mockGetAverageBalanceBetween( + address _vault, + address _user, + uint64 _startTime, + uint64 _endTime, + uint256 _result + ) internal { + vm.mockCall( + address(twabController), + abi.encodeWithSelector( + TwabController.getTwabBetween.selector, + _vault, + _user, + _startTime, + _endTime + ), + abi.encode(_result) + ); + } + + function mockGetAverageTotalSupplyBetween( + address _vault, + uint32 _startTime, + uint32 _endTime, + uint256 _result + ) internal { + vm.mockCall( + address(twabController), + abi.encodeWithSelector( + TwabController.getTotalSupplyTwabBetween.selector, + _vault, + _startTime, + _endTime + ), + abi.encode(_result) + ); + } + + function testEstimatedPrizeCount_current() public { + assertEq(prizePool.estimatedPrizeCount(), 4); + } + + function testEstimatedPrizeCount() public { + // assumes grand prize is 365 + assertEq(prizePool.estimatedPrizeCount(0), 0); + assertEq(prizePool.estimatedPrizeCount(1), 0); + assertEq(prizePool.estimatedPrizeCount(2), 0); + assertEq(prizePool.estimatedPrizeCount(3), 4); + assertEq(prizePool.estimatedPrizeCount(4), 16); + assertEq(prizePool.estimatedPrizeCount(5), 66); + assertEq(prizePool.estimatedPrizeCount(6), 270); + assertEq(prizePool.estimatedPrizeCount(7), 1108); + assertEq(prizePool.estimatedPrizeCount(8), 4517); + assertEq(prizePool.estimatedPrizeCount(9), 18358); + assertEq(prizePool.estimatedPrizeCount(10), 74435); + assertEq(prizePool.estimatedPrizeCount(11), 301239); + assertEq(prizePool.estimatedPrizeCount(12), 1217266); + assertEq(prizePool.estimatedPrizeCount(13), 4912619); + assertEq(prizePool.estimatedPrizeCount(14), 19805536); + assertEq(prizePool.estimatedPrizeCount(15), 79777187); + assertEq(prizePool.estimatedPrizeCount(16), 0); + } + + function testcanaryPrizeCountFractional() public { + // assuming 10 reserve, 10 canary, and 100 per tier + assertEq(prizePool.canaryPrizeCountFractional(0).unwrap(), 0); + assertEq(prizePool.canaryPrizeCountFractional(1).unwrap(), 0); + assertEq(prizePool.canaryPrizeCountFractional(2).unwrap(), 0); + assertEq(prizePool.canaryPrizeCountFractional(3).unwrap(), 2327272727272727264); + assertEq(prizePool.canaryPrizeCountFractional(4).unwrap(), 8400000000000000000); + assertEq(prizePool.canaryPrizeCountFractional(5).unwrap(), 31695238095238095104); + assertEq(prizePool.canaryPrizeCountFractional(6).unwrap(), 122092307692307691520); + assertEq(prizePool.canaryPrizeCountFractional(7).unwrap(), 475664516129032257536); + assertEq(prizePool.canaryPrizeCountFractional(8).unwrap(), 1865955555555555540992); + assertEq(prizePool.canaryPrizeCountFractional(9).unwrap(), 7352819512195121938432); + assertEq(prizePool.canaryPrizeCountFractional(10).unwrap(), 29063791304347825995776); + assertEq(prizePool.canaryPrizeCountFractional(11).unwrap(), 115137756862745097011200); + assertEq(prizePool.canaryPrizeCountFractional(12).unwrap(), 456879542857142854746112); + assertEq(prizePool.canaryPrizeCountFractional(13).unwrap(), 1815239763934426215481344); + assertEq(prizePool.canaryPrizeCountFractional(14).unwrap(), 7219286884848484797644800); + assertEq(prizePool.canaryPrizeCountFractional(15).unwrap(), 28733936135211267454402560); + assertEq(prizePool.canaryPrizeCountFractional(16).unwrap(), 0); + } + + function contribute(uint256 amountContributed) public { + contribute(amountContributed, address(this)); + } + + function contribute(uint256 amountContributed, address to) public { + prizeToken.mint(address(prizePool), amountContributed); + prizePool.contributePrizeTokens(to, amountContributed); + } + + function completeAndStartNextDraw(uint256 _winningRandomNumber) public { + vm.warp(prizePool.nextDrawEndsAt()); + prizePool.completeAndStartNextDraw(_winningRandomNumber); + } + + function claimPrize(address sender, uint8 tier, uint32 prizeIndex) public returns (uint256) { + return claimPrize(sender, tier, prizeIndex, 0, address(0)); + } + + function claimPrize( + address sender, + uint8 tier, + uint32 prizeIndex, + uint96 fee, + address feeRecipient + ) public returns (uint256) { + address[] memory winners = new address[](1); + winners[0] = sender; + uint32[][] memory prizeIndices = new uint32[][](1); + prizeIndices[0] = new uint32[](1); + prizeIndices[0][0] = prizeIndex; + return prizePool.claimPrizes(tier, winners, prizeIndices, fee, feeRecipient); + } + + function mockTwab(address _account, uint256 startTime, uint256 endTime) public { + // console2.log("mockTwab startTime", startTime); + // console2.log("mockTwab endTime", endTime); + mockGetAverageBalanceBetween( + address(this), + _account, + uint32(startTime), + uint32(endTime), + 366e30 + ); + mockGetAverageTotalSupplyBetween(address(this), uint32(startTime), uint32(endTime), 1e30); + } + + function mockTwab(address _account, uint8 _tier) public { + (uint64 startTime, uint64 endTime) = prizePool.calculateTierTwabTimestamps(_tier); + mockTwab(_account, startTime, endTime); + } } diff --git a/test/abstract/helper/TieredLiquidityDistributorWrapper.sol b/test/abstract/helper/TieredLiquidityDistributorWrapper.sol index 7f2f19a..bb9cad4 100644 --- a/test/abstract/helper/TieredLiquidityDistributorWrapper.sol +++ b/test/abstract/helper/TieredLiquidityDistributorWrapper.sol @@ -6,31 +6,43 @@ import "forge-std/console2.sol"; import { TieredLiquidityDistributor, Tier, fromUD34x4toUD60x18 } from "src/abstract/TieredLiquidityDistributor.sol"; contract TieredLiquidityDistributorWrapper is TieredLiquidityDistributor { - - constructor ( - uint16 _grandPrizePeriodDraws, - uint8 _numberOfTiers, - uint8 _tierShares, - uint8 _canaryShares, - uint8 _reserveShares - ) TieredLiquidityDistributor(_grandPrizePeriodDraws, _numberOfTiers, _tierShares, _canaryShares, _reserveShares) {} - - function nextDraw(uint8 _nextNumTiers, uint96 liquidity) external { - _nextDraw(_nextNumTiers, liquidity); - } - - function consumeLiquidity(uint8 _tier, uint104 _liquidity) external returns (Tier memory) { - Tier memory _tierData = _getTier(_tier, numberOfTiers); - return _consumeLiquidity(_tierData, _tier, _liquidity); - } - - function remainingTierLiquidity(uint8 _tier) external view returns (uint112) { - uint8 shares = _computeShares(_tier, numberOfTiers); - Tier memory tier = _getTier(_tier, numberOfTiers); - return _remainingTierLiquidity(tier, shares); - } - - function getTierLiquidityToReclaim(uint8 _nextNumberOfTiers) external view returns (uint256) { - return _getTierLiquidityToReclaim(numberOfTiers, _nextNumberOfTiers, fromUD34x4toUD60x18(prizeTokenPerShare)); - } + constructor( + uint16 _grandPrizePeriodDraws, + uint8 _numberOfTiers, + uint8 _tierShares, + uint8 _canaryShares, + uint8 _reserveShares + ) + TieredLiquidityDistributor( + _grandPrizePeriodDraws, + _numberOfTiers, + _tierShares, + _canaryShares, + _reserveShares + ) + {} + + function nextDraw(uint8 _nextNumTiers, uint96 liquidity) external { + _nextDraw(_nextNumTiers, liquidity); + } + + function consumeLiquidity(uint8 _tier, uint104 _liquidity) external returns (Tier memory) { + Tier memory _tierData = _getTier(_tier, numberOfTiers); + return _consumeLiquidity(_tierData, _tier, _liquidity); + } + + function remainingTierLiquidity(uint8 _tier) external view returns (uint112) { + uint8 shares = _computeShares(_tier, numberOfTiers); + Tier memory tier = _getTier(_tier, numberOfTiers); + return _remainingTierLiquidity(tier, shares); + } + + function getTierLiquidityToReclaim(uint8 _nextNumberOfTiers) external view returns (uint256) { + return + _getTierLiquidityToReclaim( + numberOfTiers, + _nextNumberOfTiers, + fromUD34x4toUD60x18(prizeTokenPerShare) + ); + } } diff --git a/test/invariants/DrawAccumulatorInvariants.t.sol b/test/invariants/DrawAccumulatorInvariants.t.sol index 4a361df..e098ee1 100644 --- a/test/invariants/DrawAccumulatorInvariants.t.sol +++ b/test/invariants/DrawAccumulatorInvariants.t.sol @@ -9,15 +9,14 @@ import { Observation } from "src/libraries/DrawAccumulatorLib.sol"; import { SD59x18, unwrap, toSD59x18, fromSD59x18 } from "prb-math/SD59x18.sol"; contract DrawAccumulatorInvariants is Test { + DrawAccumulatorFuzzHarness public accumulator; - DrawAccumulatorFuzzHarness public accumulator; + function setUp() external { + accumulator = new DrawAccumulatorFuzzHarness(); + } - function setUp() external { - accumulator = new DrawAccumulatorFuzzHarness(); - } - - function invariant_future_plus_past_equals_total() external { - Observation memory obs = accumulator.newestObservation(); - assertEq(obs.available + obs.disbursed, accumulator.totalAdded()); - } + function invariant_future_plus_past_equals_total() external { + Observation memory obs = accumulator.newestObservation(); + assertEq(obs.available + obs.disbursed, accumulator.totalAdded()); + } } diff --git a/test/invariants/PrizePoolInvariants.t.sol b/test/invariants/PrizePoolInvariants.t.sol index 96913c4..33d4935 100644 --- a/test/invariants/PrizePoolInvariants.t.sol +++ b/test/invariants/PrizePoolInvariants.t.sol @@ -7,15 +7,18 @@ import "forge-std/console2.sol"; import { PrizePoolFuzzHarness } from "./helpers/PrizePoolFuzzHarness.sol"; contract PrizePoolInvariants is Test { + PrizePoolFuzzHarness public prizePoolHarness; - PrizePoolFuzzHarness public prizePoolHarness; + function setUp() external { + prizePoolHarness = new PrizePoolFuzzHarness(); + } - function setUp() external { - prizePoolHarness = new PrizePoolFuzzHarness(); - } - - function invariant_balance_equals_accountedBalance() external { - uint balance = prizePoolHarness.token().balanceOf(address(prizePoolHarness.prizePool())); - assertEq(balance, prizePoolHarness.prizePool().accountedBalance(), "balance does not match accountedBalance"); - } + function invariant_balance_equals_accountedBalance() external { + uint balance = prizePoolHarness.token().balanceOf(address(prizePoolHarness.prizePool())); + assertEq( + balance, + prizePoolHarness.prizePool().accountedBalance(), + "balance does not match accountedBalance" + ); + } } diff --git a/test/invariants/TierCalculationInvariants.t.sol b/test/invariants/TierCalculationInvariants.t.sol index e478503..6ccbdb3 100644 --- a/test/invariants/TierCalculationInvariants.t.sol +++ b/test/invariants/TierCalculationInvariants.t.sol @@ -9,25 +9,37 @@ import { TierCalculationFuzzHarness } from "./helpers/TierCalculationFuzzHarness import { SD59x18, unwrap, toSD59x18, fromSD59x18 } from "prb-math/SD59x18.sol"; contract TierCalculationInvariants is Test { - - TierCalculationFuzzHarness harness; - - function setUp() public { - harness = new TierCalculationFuzzHarness(); - } - - function test_it() public { - // uint iterations = 100; - - // for (uint i = 0; i < iterations; i++) { - // console2.log("drawPrizes", harness.nextDraw(uint256(keccak256(abi.encode(i))))); - // } - - if (harness.draws() > 0) { - uint estimatedPrizeCount = TierCalculationLib.estimatedClaimCount(harness.numberOfTiers(), harness.grandPrizePeriod()); - uint bounds = 30; - console2.log("harness.averagePrizesPerDraw()", harness.averagePrizesPerDraw(), "estimatedPrizeCount", estimatedPrizeCount); - assertApproxEqAbs(harness.averagePrizesPerDraw(), estimatedPrizeCount, bounds, "estimated prizes match reality"); - } + TierCalculationFuzzHarness harness; + + function setUp() public { + harness = new TierCalculationFuzzHarness(); + } + + function test_it() public { + // uint iterations = 100; + + // for (uint i = 0; i < iterations; i++) { + // console2.log("drawPrizes", harness.nextDraw(uint256(keccak256(abi.encode(i))))); + // } + + if (harness.draws() > 0) { + uint estimatedPrizeCount = TierCalculationLib.estimatedClaimCount( + harness.numberOfTiers(), + harness.grandPrizePeriod() + ); + uint bounds = 30; + console2.log( + "harness.averagePrizesPerDraw()", + harness.averagePrizesPerDraw(), + "estimatedPrizeCount", + estimatedPrizeCount + ); + assertApproxEqAbs( + harness.averagePrizesPerDraw(), + estimatedPrizeCount, + bounds, + "estimated prizes match reality" + ); } + } } diff --git a/test/invariants/TieredLiquidityDistributorInvariants.t.sol b/test/invariants/TieredLiquidityDistributorInvariants.t.sol index c0c8451..347eb0b 100644 --- a/test/invariants/TieredLiquidityDistributorInvariants.t.sol +++ b/test/invariants/TieredLiquidityDistributorInvariants.t.sol @@ -8,45 +8,44 @@ import { UD60x18, toUD60x18, fromUD60x18 } from "prb-math/UD60x18.sol"; import { TieredLiquidityDistributorFuzzHarness } from "./helpers/TieredLiquidityDistributorFuzzHarness.sol"; contract TieredLiquidityDistributorInvariants is Test { - - TieredLiquidityDistributorFuzzHarness public distributor; - - function setUp() external { - distributor = new TieredLiquidityDistributorFuzzHarness(); - } - - function invariant_tiers_always_sum() external { - uint256 expected = distributor.totalAdded() - distributor.totalConsumed(); - uint256 accounted = distributor.accountedLiquidity(); - - // Uncomment to append delta data to local CSV file: - // -------------------------------------------------------- - // uint256 delta = expected > accounted ? expected - accounted : accounted - expected; - // vm.writeLine(string.concat(vm.projectRoot(), "/data/tiers_accounted_liquidity_delta.csv"), string.concat(vm.toString(distributor.numberOfTiers()), ",", vm.toString(delta))); - // assertApproxEqAbs(accounted, expected, 50); // run with high ceiling to avoid failures while recording data - // -------------------------------------------------------- - // Comment out to avoid failing test while recording data: - assertEq(accounted, expected, "accounted equals expected"); - // -------------------------------------------------------- - } - - // Failure case regression test (2023-05-26) - function testInvariantFailure_Case_2023_05_26() external { - distributor.nextDraw(4, 253012247290373118207); - distributor.nextDraw(3, 99152290762372054017); - distributor.nextDraw(255, 79228162514264337593543950333); - distributor.consumeLiquidity(1); - distributor.consumeLiquidity(0); - distributor.nextDraw(1, 2365); - distributor.nextDraw(5, 36387); - distributor.nextDraw(74, 486356342973499764); - distributor.consumeLiquidity(174); - distributor.consumeLiquidity(254); - distributor.nextDraw(6, 2335051495798885129312); - distributor.nextDraw(160, 543634559793817062402422965); - distributor.nextDraw(187, 3765046993999626249); - distributor.nextDraw(1, 196958881398058173458); - uint256 expected = distributor.totalAdded() - distributor.totalConsumed(); - assertEq(distributor.accountedLiquidity(), expected); - } + TieredLiquidityDistributorFuzzHarness public distributor; + + function setUp() external { + distributor = new TieredLiquidityDistributorFuzzHarness(); + } + + function invariant_tiers_always_sum() external { + uint256 expected = distributor.totalAdded() - distributor.totalConsumed(); + uint256 accounted = distributor.accountedLiquidity(); + + // Uncomment to append delta data to local CSV file: + // -------------------------------------------------------- + // uint256 delta = expected > accounted ? expected - accounted : accounted - expected; + // vm.writeLine(string.concat(vm.projectRoot(), "/data/tiers_accounted_liquidity_delta.csv"), string.concat(vm.toString(distributor.numberOfTiers()), ",", vm.toString(delta))); + // assertApproxEqAbs(accounted, expected, 50); // run with high ceiling to avoid failures while recording data + // -------------------------------------------------------- + // Comment out to avoid failing test while recording data: + assertEq(accounted, expected, "accounted equals expected"); + // -------------------------------------------------------- + } + + // Failure case regression test (2023-05-26) + function testInvariantFailure_Case_2023_05_26() external { + distributor.nextDraw(4, 253012247290373118207); + distributor.nextDraw(3, 99152290762372054017); + distributor.nextDraw(255, 79228162514264337593543950333); + distributor.consumeLiquidity(1); + distributor.consumeLiquidity(0); + distributor.nextDraw(1, 2365); + distributor.nextDraw(5, 36387); + distributor.nextDraw(74, 486356342973499764); + distributor.consumeLiquidity(174); + distributor.consumeLiquidity(254); + distributor.nextDraw(6, 2335051495798885129312); + distributor.nextDraw(160, 543634559793817062402422965); + distributor.nextDraw(187, 3765046993999626249); + distributor.nextDraw(1, 196958881398058173458); + uint256 expected = distributor.totalAdded() - distributor.totalConsumed(); + assertEq(distributor.accountedLiquidity(), expected); + } } diff --git a/test/invariants/helpers/DrawAccumulatorFuzzHarness.sol b/test/invariants/helpers/DrawAccumulatorFuzzHarness.sol index 078fbf2..a7fdf56 100644 --- a/test/invariants/helpers/DrawAccumulatorFuzzHarness.sol +++ b/test/invariants/helpers/DrawAccumulatorFuzzHarness.sol @@ -7,29 +7,28 @@ import { DrawAccumulatorLib, Observation } from "src/libraries/DrawAccumulatorLi import { E, SD59x18, sd, unwrap, toSD59x18, fromSD59x18 } from "prb-math/SD59x18.sol"; contract DrawAccumulatorFuzzHarness { - using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator; + using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator; - uint256 public totalAdded; + uint256 public totalAdded; - DrawAccumulatorLib.Accumulator internal accumulator; + DrawAccumulatorLib.Accumulator internal accumulator; - uint16 currentDrawId = 1; + uint16 currentDrawId = 1; - function add(uint64 _amount, uint8 _drawInc) public returns (bool) { - currentDrawId += (_drawInc / 16); - console2.log("currentDrawId", currentDrawId); - SD59x18 alpha = sd(0.9e18); - bool result = accumulator.add(_amount, currentDrawId, alpha); - totalAdded += _amount; - return result; - } + function add(uint64 _amount, uint8 _drawInc) public returns (bool) { + currentDrawId += (_drawInc / 16); + console2.log("currentDrawId", currentDrawId); + SD59x18 alpha = sd(0.9e18); + bool result = accumulator.add(_amount, currentDrawId, alpha); + totalAdded += _amount; + return result; + } - function newestObservation() external view returns (Observation memory) { - return accumulator.newestObservation(); - } - - function newestDrawId() external view returns (uint256) { - return accumulator.newestDrawId(); - } + function newestObservation() external view returns (Observation memory) { + return accumulator.newestObservation(); + } + function newestDrawId() external view returns (uint256) { + return accumulator.newestDrawId(); + } } diff --git a/test/invariants/helpers/PrizePoolFuzzHarness.sol b/test/invariants/helpers/PrizePoolFuzzHarness.sol index a103e86..b94c282 100644 --- a/test/invariants/helpers/PrizePoolFuzzHarness.sol +++ b/test/invariants/helpers/PrizePoolFuzzHarness.sol @@ -11,77 +11,78 @@ import { SD1x18 } from "prb-math/SD1x18.sol"; import { CommonBase } from "forge-std/Base.sol"; contract PrizePoolFuzzHarness is CommonBase { + PrizePool public prizePool; + ERC20Mintable public token; - PrizePool public prizePool; - ERC20Mintable public token; + uint public contributed; + uint public withdrawn; + uint public claimed; - uint public contributed; - uint public withdrawn; - uint public claimed; + constructor() { + address drawManager = address(this); + uint16 grandPrizePeriodDraws = 10; + uint32 drawPeriodSeconds = 1 hours; + uint64 nextDrawStartsAt = uint64(block.timestamp); + uint8 numberOfTiers = 3; + uint8 tierShares = 100; + uint8 canaryShares = 10; + uint8 reserveShares = 10; + UD2x18 claimExpansionThreshold = UD2x18.wrap(0.9e18); + SD1x18 smoothing = SD1x18.wrap(0.9e18); - constructor() { - address drawManager = address(this); - uint16 grandPrizePeriodDraws = 10; - uint32 drawPeriodSeconds = 1 hours; - uint64 nextDrawStartsAt = uint64(block.timestamp); - uint8 numberOfTiers = 3; - uint8 tierShares = 100; - uint8 canaryShares = 10; - uint8 reserveShares = 10; - UD2x18 claimExpansionThreshold = UD2x18.wrap(0.9e18); - SD1x18 smoothing = SD1x18.wrap(0.9e18); + token = new ERC20Mintable("name", "SYMBOL"); + TwabController twabController = new TwabController(); + // arbitrary mint + twabController.mint(address(this), 100e18); - token = new ERC20Mintable("name", "SYMBOL"); - TwabController twabController = new TwabController(); - // arbitrary mint - twabController.mint(address(this), 100e18); + ConstructorParams memory params = ConstructorParams( + token, + twabController, + drawManager, + grandPrizePeriodDraws, + drawPeriodSeconds, + nextDrawStartsAt, + numberOfTiers, + tierShares, + canaryShares, + reserveShares, + claimExpansionThreshold, + smoothing + ); + prizePool = new PrizePool(params); + } - ConstructorParams memory params = ConstructorParams( - token, - twabController, - drawManager, - grandPrizePeriodDraws, - drawPeriodSeconds, - nextDrawStartsAt, - numberOfTiers, - tierShares, - canaryShares, - reserveShares, - claimExpansionThreshold, - smoothing - ); - prizePool = new PrizePool(params); - } - - function contributePrizeTokens(uint64 _amount) public { - contributed += _amount; - token.mint(address(prizePool), _amount); - prizePool.contributePrizeTokens(address(this), _amount); - } + function contributePrizeTokens(uint64 _amount) public { + contributed += _amount; + token.mint(address(prizePool), _amount); + prizePool.contributePrizeTokens(address(this), _amount); + } - function withdrawReserve(uint64 amount) public { - withdrawn += amount; - vm.assume(amount <= prizePool.reserve()); - prizePool.withdrawReserve(address(msg.sender), uint104(amount)); - } + function withdrawReserve(uint64 amount) public { + withdrawn += amount; + vm.assume(amount <= prizePool.reserve()); + prizePool.withdrawReserve(address(msg.sender), uint104(amount)); + } - function claimPrizes() public { - for (uint8 i = 0; i < prizePool.numberOfTiers(); i++) { - for (uint32 p = 0; p < prizePool.getTierPrizeCount(i); i++) { - if (prizePool.isWinner(address(this), address(this), i, p) && !prizePool.wasClaimed(address(this), i, p)) { - address[] memory winners = new address[](1); - winners[0] = address(this); - uint32[][] memory prizeIndices = new uint32[][](1); - prizeIndices[0] = new uint32[](1); - prizeIndices[0][0] = p; - claimed += prizePool.claimPrizes(i, winners, prizeIndices, 0, address(0)); - } - } + function claimPrizes() public { + for (uint8 i = 0; i < prizePool.numberOfTiers(); i++) { + for (uint32 p = 0; p < prizePool.getTierPrizeCount(i); i++) { + if ( + prizePool.isWinner(address(this), address(this), i, p) && + !prizePool.wasClaimed(address(this), i, p) + ) { + address[] memory winners = new address[](1); + winners[0] = address(this); + uint32[][] memory prizeIndices = new uint32[][](1); + prizeIndices[0] = new uint32[](1); + prizeIndices[0][0] = p; + claimed += prizePool.claimPrizes(i, winners, prizeIndices, 0, address(0)); } + } } + } - function completeDraw() public { - prizePool.completeAndStartNextDraw(uint256(keccak256(abi.encode(block.timestamp)))); - } - + function completeDraw() public { + prizePool.completeAndStartNextDraw(uint256(keccak256(abi.encode(block.timestamp)))); + } } diff --git a/test/invariants/helpers/TierCalculationFuzzHarness.sol b/test/invariants/helpers/TierCalculationFuzzHarness.sol index 65292b3..d79e410 100644 --- a/test/invariants/helpers/TierCalculationFuzzHarness.sol +++ b/test/invariants/helpers/TierCalculationFuzzHarness.sol @@ -8,38 +8,49 @@ import { SD59x18, unwrap, toSD59x18, fromSD59x18 } from "prb-math/SD59x18.sol"; import { CommonBase } from "forge-std/Base.sol"; contract TierCalculationFuzzHarness is CommonBase { - - uint8 public immutable grandPrizePeriod = 10; - uint128 immutable eachUserBalance = 100e18; - SD59x18 immutable vaultPortion = toSD59x18(1); - uint8 immutable _userCount = 20; - uint8 public immutable numberOfTiers = 5; - - uint public winnerCount; - uint32 public draws; - - function nextDraw(uint256 winningRandomNumber) public returns (uint) { - uint drawPrizeCount; - for (uint8 t = 0; t < numberOfTiers; t++) { - uint32 prizeCount = uint32(TierCalculationLib.prizeCount(t)); - SD59x18 tierOdds = TierCalculationLib.getTierOdds(t, numberOfTiers, grandPrizePeriod); - for (uint u = 1; u < _userCount+1; u++) { - address userAddress = vm.addr(u); - for (uint32 p = 0; p < prizeCount; p++) { - uint256 prn = TierCalculationLib.calculatePseudoRandomNumber(userAddress, t, p, winningRandomNumber); - if (TierCalculationLib.isWinner(prn, eachUserBalance, _userCount*eachUserBalance, vaultPortion, tierOdds)) { - drawPrizeCount++; - } - } - } + uint8 public immutable grandPrizePeriod = 10; + uint128 immutable eachUserBalance = 100e18; + SD59x18 immutable vaultPortion = toSD59x18(1); + uint8 immutable _userCount = 20; + uint8 public immutable numberOfTiers = 5; + + uint public winnerCount; + uint32 public draws; + + function nextDraw(uint256 winningRandomNumber) public returns (uint) { + uint drawPrizeCount; + for (uint8 t = 0; t < numberOfTiers; t++) { + uint32 prizeCount = uint32(TierCalculationLib.prizeCount(t)); + SD59x18 tierOdds = TierCalculationLib.getTierOdds(t, numberOfTiers, grandPrizePeriod); + for (uint u = 1; u < _userCount + 1; u++) { + address userAddress = vm.addr(u); + for (uint32 p = 0; p < prizeCount; p++) { + uint256 prn = TierCalculationLib.calculatePseudoRandomNumber( + userAddress, + t, + p, + winningRandomNumber + ); + if ( + TierCalculationLib.isWinner( + prn, + eachUserBalance, + _userCount * eachUserBalance, + vaultPortion, + tierOdds + ) + ) { + drawPrizeCount++; + } } - winnerCount += drawPrizeCount; - draws++; - return drawPrizeCount; + } } - - function averagePrizesPerDraw() public view returns (uint256) { - return winnerCount / draws; - } - + winnerCount += drawPrizeCount; + draws++; + return drawPrizeCount; + } + + function averagePrizesPerDraw() public view returns (uint256) { + return winnerCount / draws; + } } diff --git a/test/invariants/helpers/TieredLiquidityDistributorFuzzHarness.sol b/test/invariants/helpers/TieredLiquidityDistributorFuzzHarness.sol index c81c469..e176372 100644 --- a/test/invariants/helpers/TieredLiquidityDistributorFuzzHarness.sol +++ b/test/invariants/helpers/TieredLiquidityDistributorFuzzHarness.sol @@ -3,59 +3,51 @@ pragma solidity 0.8.17; import "forge-std/console2.sol"; -import { - TieredLiquidityDistributor, - Tier, - fromUD60x18, - toUD60x18, - fromUD34x4toUD60x18 -} from "src/abstract/TieredLiquidityDistributor.sol"; +import { TieredLiquidityDistributor, Tier, fromUD60x18, toUD60x18, fromUD34x4toUD60x18 } from "src/abstract/TieredLiquidityDistributor.sol"; contract TieredLiquidityDistributorFuzzHarness is TieredLiquidityDistributor { - - uint256 public totalAdded; - uint256 public totalConsumed; - - constructor () TieredLiquidityDistributor(10, 3, 100, 10, 10) {} - - function nextDraw(uint8 _nextNumTiers, uint96 liquidity) external { - uint8 nextNumTiers = _nextNumTiers / 16; // map to [0, 15] - nextNumTiers = nextNumTiers < 3 ? 3 : nextNumTiers; // ensure min tiers - totalAdded += liquidity; - // console2.log("nextDraw", nextNumTiers, liquidity); - _nextDraw(nextNumTiers, liquidity); + uint256 public totalAdded; + uint256 public totalConsumed; + + constructor() TieredLiquidityDistributor(10, 3, 100, 10, 10) {} + + function nextDraw(uint8 _nextNumTiers, uint96 liquidity) external { + uint8 nextNumTiers = _nextNumTiers / 16; // map to [0, 15] + nextNumTiers = nextNumTiers < 3 ? 3 : nextNumTiers; // ensure min tiers + totalAdded += liquidity; + // console2.log("nextDraw", nextNumTiers, liquidity); + _nextDraw(nextNumTiers, liquidity); + } + + function net() external view returns (uint256) { + return totalAdded - totalConsumed; + } + + function accountedLiquidity() external view returns (uint256) { + uint256 availableLiquidity; + for (uint8 i = 0; i < numberOfTiers; i++) { + Tier memory tier = _getTier(i, numberOfTiers); + availableLiquidity += _remainingTierLiquidity(tier, _computeShares(i, numberOfTiers)); } - - function net() external view returns (uint256) { - return totalAdded - totalConsumed; - } - - function accountedLiquidity() external view returns (uint256) { - uint256 availableLiquidity; - for (uint8 i = 0; i < numberOfTiers; i++) { - Tier memory tier = _getTier(i, numberOfTiers); - availableLiquidity += _remainingTierLiquidity(tier, _computeShares(i, numberOfTiers)); - } - // console2.log("reserve ", _reserve); - availableLiquidity += _reserve; - // console2.log("SUM", availableLiquidity); - return availableLiquidity; - } - - function consumeLiquidity(uint8 _tier) external { - uint8 tier = _tier % numberOfTiers; - - Tier memory tier_ = _getTier(tier, numberOfTiers); - uint8 shares = _computeShares(tier, numberOfTiers); - uint112 liq = _remainingTierLiquidity(tier_, shares); - - // half the time consume only half - if (_tier > 128) { - liq += _reserve / 2; - } - - totalConsumed += liq; - tier_ = _consumeLiquidity(tier_, tier, uint104(liq)); + // console2.log("reserve ", _reserve); + availableLiquidity += _reserve; + // console2.log("SUM", availableLiquidity); + return availableLiquidity; + } + + function consumeLiquidity(uint8 _tier) external { + uint8 tier = _tier % numberOfTiers; + + Tier memory tier_ = _getTier(tier, numberOfTiers); + uint8 shares = _computeShares(tier, numberOfTiers); + uint112 liq = _remainingTierLiquidity(tier_, shares); + + // half the time consume only half + if (_tier > 128) { + liq += _reserve / 2; } + totalConsumed += liq; + tier_ = _consumeLiquidity(tier_, tier, uint104(liq)); + } } diff --git a/test/libraries/BitLib.t.sol b/test/libraries/BitLib.t.sol index bcf2ade..9aa7e36 100644 --- a/test/libraries/BitLib.t.sol +++ b/test/libraries/BitLib.t.sol @@ -6,49 +6,47 @@ import "forge-std/Test.sol"; import { BitLibWrapper } from "test/wrappers/BitLibWrapper.sol"; contract BitLibTest is Test { - - BitLibWrapper bitLib; - - function setUp() public { - bitLib = new BitLibWrapper(); - } - - function testGetBit_zero() public { - assertEq(bitLib.getBit(0x1, 0), true); - } - - function testGetBit_three() public { - assertEq(bitLib.getBit(0x4, 2), true); - } - - function testGetBit_five() public { - assertEq(bitLib.getBit(0x10, 4), true); - } - - function testGetBit_last() public { - assertEq(bitLib.getBit(0x8000000000000000000000000000000000000000000000000000000000000000, 255), true); - } - - function testFlipBit_zero() public { - assertEq(bitLib.flipBit(0x1, 0), 0x0); - } - - function testFlipBit_five() public { - assertEq(bitLib.flipBit(0x11, 4), 0x01); - } - - function testFlipBit_last() public { - assertEq( - bitLib.flipBit(0x8f0000000000000000000000000000000000000000000000000000000000000f, 255), - 0x0f0000000000000000000000000000000000000000000000000000000000000f - ); - } - - function testFlipBit_cast() public { - assertEq( - bitLib.flipBit(uint8(0x8f), 7), - 0x0f - ); - } - + BitLibWrapper bitLib; + + function setUp() public { + bitLib = new BitLibWrapper(); + } + + function testGetBit_zero() public { + assertEq(bitLib.getBit(0x1, 0), true); + } + + function testGetBit_three() public { + assertEq(bitLib.getBit(0x4, 2), true); + } + + function testGetBit_five() public { + assertEq(bitLib.getBit(0x10, 4), true); + } + + function testGetBit_last() public { + assertEq( + bitLib.getBit(0x8000000000000000000000000000000000000000000000000000000000000000, 255), + true + ); + } + + function testFlipBit_zero() public { + assertEq(bitLib.flipBit(0x1, 0), 0x0); + } + + function testFlipBit_five() public { + assertEq(bitLib.flipBit(0x11, 4), 0x01); + } + + function testFlipBit_last() public { + assertEq( + bitLib.flipBit(0x8f0000000000000000000000000000000000000000000000000000000000000f, 255), + 0x0f0000000000000000000000000000000000000000000000000000000000000f + ); + } + + function testFlipBit_cast() public { + assertEq(bitLib.flipBit(uint8(0x8f), 7), 0x0f); + } } diff --git a/test/libraries/DrawAccumulatorLib.t.sol b/test/libraries/DrawAccumulatorLib.t.sol index 45f5790..d0c1b02 100644 --- a/test/libraries/DrawAccumulatorLib.t.sol +++ b/test/libraries/DrawAccumulatorLib.t.sol @@ -8,16 +8,16 @@ import { DrawAccumulatorLibWrapper } from "test/wrappers/DrawAccumulatorLibWrapp import { SD59x18, sd } from "prb-math/SD59x18.sol"; contract DrawAccumulatorLibTest is Test { - using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator; + using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator; - DrawAccumulatorLib.Accumulator accumulator; - DrawAccumulatorLibWrapper wrapper; - SD59x18 alpha; - uint contribution = 10000; + DrawAccumulatorLib.Accumulator accumulator; + DrawAccumulatorLibWrapper wrapper; + SD59x18 alpha; + uint contribution = 10000; - function setUp() public { - alpha = sd(0.9e18); - /* + function setUp() public { + alpha = sd(0.9e18); + /* Alpha of 0.9 and contribution of 10000 result in disbursal of: 1 1000 @@ -28,279 +28,296 @@ contract DrawAccumulatorLibTest is Test { 6 590 ... */ - wrapper = new DrawAccumulatorLibWrapper(); - } - - function testAdd_emitsAddToDrawZero() public { - vm.expectRevert(abi.encodeWithSelector(AddToDrawZero.selector)); - add(0); - } - - function testAdd_emitsDrawCLosed() public { - add(4); - vm.expectRevert(abi.encodeWithSelector(DrawClosed.selector, 3, 4)); - add(3); - } - - function testAddOne() public { - DrawAccumulatorLib.add(accumulator, 100, 1, alpha); - assertEq(accumulator.ringBufferInfo.cardinality, 1); - assertEq(accumulator.ringBufferInfo.nextIndex, 1); - assertEq(accumulator.drawRingBuffer[0], 1); - assertEq(accumulator.observations[1].available, 100); - } - - function testAddSame() public { - DrawAccumulatorLib.add(accumulator, 100, 1, alpha); - DrawAccumulatorLib.add(accumulator, 200, 1, alpha); - - assertEq(accumulator.ringBufferInfo.cardinality, 1); - assertEq(accumulator.ringBufferInfo.nextIndex, 1); - assertEq(accumulator.drawRingBuffer[0], 1); - assertEq(accumulator.observations[1].available, 300); - } - - function testAddSecond() public { - DrawAccumulatorLib.add(accumulator, 100, 1, alpha); - DrawAccumulatorLib.add(accumulator, 200, 3, alpha); - - assertEq(accumulator.ringBufferInfo.cardinality, 2); - assertEq(accumulator.ringBufferInfo.nextIndex, 2); - assertEq(accumulator.drawRingBuffer[0], 1); - assertEq(accumulator.drawRingBuffer[1], 3); - - // 100 - 19 = 81 - - assertEq(accumulator.observations[3].available, 281); - } - - function testGetTotalRemaining() public { - add(1); - - assertEq(wrapper.getTotalRemaining(2, alpha), 9000); - } - - function testGetTotalRemaining_empty() public { - assertEq(wrapper.getTotalRemaining(1, alpha), 0); - } - - function testGetTotalRemaining_invalidDraw() public { - add(4); - vm.expectRevert(abi.encodeWithSelector(DrawClosed.selector, 2, 4)); - wrapper.getTotalRemaining(2, alpha); - } - - function testGetDisbursedBetweenEmpty() public { - assertEq(getDisbursedBetween(1, 4), 0); - } - - function testGetDisbursedBetween_invalidRange() public { - vm.expectRevert(abi.encodeWithSelector(InvalidDrawRange.selector, 2, 1)); - getDisbursedBetween(2, 1); - } - - function testGetDisbursedBetween_invalidEnd() public { - add(3); - vm.expectRevert(abi.encodeWithSelector(InvalidDisbursedEndDrawId.selector, 1)); - getDisbursedBetween(1, 1); - } - - function testGetDisbursedBetween_onOne() public { - add(1); - /* + wrapper = new DrawAccumulatorLibWrapper(); + } + + function testAdd_emitsAddToDrawZero() public { + vm.expectRevert(abi.encodeWithSelector(AddToDrawZero.selector)); + add(0); + } + + function testAdd_emitsDrawCLosed() public { + add(4); + vm.expectRevert(abi.encodeWithSelector(DrawClosed.selector, 3, 4)); + add(3); + } + + function testAddOne() public { + DrawAccumulatorLib.add(accumulator, 100, 1, alpha); + assertEq(accumulator.ringBufferInfo.cardinality, 1); + assertEq(accumulator.ringBufferInfo.nextIndex, 1); + assertEq(accumulator.drawRingBuffer[0], 1); + assertEq(accumulator.observations[1].available, 100); + } + + function testAddSame() public { + DrawAccumulatorLib.add(accumulator, 100, 1, alpha); + DrawAccumulatorLib.add(accumulator, 200, 1, alpha); + + assertEq(accumulator.ringBufferInfo.cardinality, 1); + assertEq(accumulator.ringBufferInfo.nextIndex, 1); + assertEq(accumulator.drawRingBuffer[0], 1); + assertEq(accumulator.observations[1].available, 300); + } + + function testAddSecond() public { + DrawAccumulatorLib.add(accumulator, 100, 1, alpha); + DrawAccumulatorLib.add(accumulator, 200, 3, alpha); + + assertEq(accumulator.ringBufferInfo.cardinality, 2); + assertEq(accumulator.ringBufferInfo.nextIndex, 2); + assertEq(accumulator.drawRingBuffer[0], 1); + assertEq(accumulator.drawRingBuffer[1], 3); + + // 100 - 19 = 81 + + assertEq(accumulator.observations[3].available, 281); + } + + function testGetTotalRemaining() public { + add(1); + + assertEq(wrapper.getTotalRemaining(2, alpha), 9000); + } + + function testGetTotalRemaining_empty() public { + assertEq(wrapper.getTotalRemaining(1, alpha), 0); + } + + function testGetTotalRemaining_invalidDraw() public { + add(4); + vm.expectRevert(abi.encodeWithSelector(DrawClosed.selector, 2, 4)); + wrapper.getTotalRemaining(2, alpha); + } + + function testGetDisbursedBetweenEmpty() public { + assertEq(getDisbursedBetween(1, 4), 0); + } + + function testGetDisbursedBetween_invalidRange() public { + vm.expectRevert(abi.encodeWithSelector(InvalidDrawRange.selector, 2, 1)); + getDisbursedBetween(2, 1); + } + + function testGetDisbursedBetween_invalidEnd() public { + add(3); + vm.expectRevert(abi.encodeWithSelector(InvalidDisbursedEndDrawId.selector, 1)); + getDisbursedBetween(1, 1); + } + + function testGetDisbursedBetween_onOne() public { + add(1); + /* should include draw 1, 2, 3 and 4: 1 1000 2 900 3 810 4 729 */ - assertEq(getDisbursedBetween(1, 4), 3438); - } - - function testGetDisbursedBetween_beforeOne() public { - add(4); - // should include draw 2, 3 and 4 - assertEq(getDisbursedBetween(2, 3), 0); - } - - function testGetDisbursedBetween_endOnOne() public { - add(4); - // should include draw 2, 3 and 4 - assertEq(getDisbursedBetween(2, 4), 1000); - } - - function testGetDisbursedBetween_startOnOne() public { - add(4); - // should include draw 2, 3 and 4 - assertEq(getDisbursedBetween(4, 4), 1000); - } - - function testGetDisbursedBetween_afterOne() public { - add(1); - // should include draw 2, 3 and 4 - assertEq(getDisbursedBetween(2, 4), 2438); - } - - function testGetDisbursedBetween_beforeOnTwo() public { - add(4); - add(5); - /* + assertEq(getDisbursedBetween(1, 4), 3438); + } + + function testGetDisbursedBetween_beforeOne() public { + add(4); + // should include draw 2, 3 and 4 + assertEq(getDisbursedBetween(2, 3), 0); + } + + function testGetDisbursedBetween_endOnOne() public { + add(4); + // should include draw 2, 3 and 4 + assertEq(getDisbursedBetween(2, 4), 1000); + } + + function testGetDisbursedBetween_startOnOne() public { + add(4); + // should include draw 2, 3 and 4 + assertEq(getDisbursedBetween(4, 4), 1000); + } + + function testGetDisbursedBetween_afterOne() public { + add(1); + // should include draw 2, 3 and 4 + assertEq(getDisbursedBetween(2, 4), 2438); + } + + function testGetDisbursedBetween_beforeOnTwo() public { + add(4); + add(5); + /* should include draw 1, 2, 3 and 4: 1 1000 2 900 3 810 + 1000 4 729 + 900 */ - assertEq(getDisbursedBetween(1, 4), 1000); - } + assertEq(getDisbursedBetween(1, 4), 1000); + } - function testGetDisbursedBetween_aroundFirstOfTwo() public { - add(4); - add(6); - /* + function testGetDisbursedBetween_aroundFirstOfTwo() public { + add(4); + add(6); + /* should include draw 1, 2, 3 and 4: 1 1000 2 900 3 810 + 1000 4 729 + 900 */ - assertEq(getDisbursedBetween(1, 5), 1899); - } + assertEq(getDisbursedBetween(1, 5), 1899); + } - function testGetDisbursedBetween_acrossTwo() public { - add(2); - add(4); - /* + function testGetDisbursedBetween_acrossTwo() public { + add(2); + add(4); + /* should include draw 2, 3 and 4: 2 1000 3 900 4 810 + 1000 5 729 + 900 */ - assertEq(getDisbursedBetween(1, 4), 3710); - } + assertEq(getDisbursedBetween(1, 4), 3710); + } - function testGetDisbursedBetween_onOneBetweenTwo() public { - add(2); - add(4); - /* + function testGetDisbursedBetween_onOneBetweenTwo() public { + add(2); + add(4); + /* should include draw 2, 3 and 4: 2 1000 3 900 4 810 + 1000 5 729 + 900 */ - assertEq(getDisbursedBetween(3, 3), 899); - } + assertEq(getDisbursedBetween(3, 3), 899); + } - function testGetDisbursedBetween_betweenTwo() public { - add(1); - add(4); - /* + function testGetDisbursedBetween_betweenTwo() public { + add(1); + add(4); + /* should include draw 1, 2, 3 and 4: 1 1000 2 900 3 810 4 729 + 1000 */ - assertEq(getDisbursedBetween(2, 3), 1709); - } + assertEq(getDisbursedBetween(2, 3), 1709); + } - function testGetDisbursedBetween_aroundLastOfTwo() public { - add(1); - add(4); - /* + function testGetDisbursedBetween_aroundLastOfTwo() public { + add(1); + add(4); + /* should include draw 1, 2, 3 and 4: 1 1000 2 900 3 810 4 729 + 1000 */ - assertEq(getDisbursedBetween(3, 4), 2538); - } - - function testIntegrateInf() public { - assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 0, 100), 100); - assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 1, 100), 90); - assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 2, 100), 81); - assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 3, 100), 72); - } - - function testIntegrate() public { - assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 0, 1, 10000), 1000); - assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 1, 2, 10000), 899); - assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 2, 3, 10000), 809); - assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 3, 4, 10000), 728); - } - - function testBinarySearchTwoWithFirstMatchingTarget() public { - fillDrawRingBuffer([1, 3, 0, 0, 0]); - (uint16 beforeOrAtIndex, uint16 beforeOrAtDrawId, uint16 afterOrAtIndex, uint16 afterOrAtDrawId) = wrapper.binarySearch( - 0, 2, 2, 1 - ); - assertEq(beforeOrAtIndex, 0); - assertEq(beforeOrAtDrawId, 1); - assertEq(afterOrAtIndex, 1); - assertEq(afterOrAtDrawId, 3); - } - - function testBinarySearchMatchingTarget() public { - fillDrawRingBuffer([1, 2, 3, 4, 5]); - (uint16 beforeOrAtIndex, uint16 beforeOrAtDrawId, uint16 afterOrAtIndex, uint16 afterOrAtDrawId) = wrapper.binarySearch( - 0, 4, 5, 3 - ); - assertEq(beforeOrAtIndex, 2); - assertEq(beforeOrAtDrawId, 3); - assertEq(afterOrAtIndex, 3); - assertEq(afterOrAtDrawId, 4); - } - - function testBinarySearchFirstMatchingTarget() public { - fillDrawRingBuffer([1, 2, 3, 4, 5]); - (uint16 beforeOrAtIndex, uint16 beforeOrAtDrawId, uint16 afterOrAtIndex, uint16 afterOrAtDrawId) = wrapper.binarySearch( - 0, 4, 5, 1 - ); - assertEq(beforeOrAtIndex, 0); - assertEq(beforeOrAtDrawId, 1); - assertEq(afterOrAtIndex, 1); - assertEq(afterOrAtDrawId, 2); - } - - function testBinarySearchLastMatchingTarget() public { - fillDrawRingBuffer([1, 2, 3, 4, 5]); - (uint16 beforeOrAtIndex, uint16 beforeOrAtDrawId, uint16 afterOrAtIndex, uint16 afterOrAtDrawId) = wrapper.binarySearch( - 0, 4, 5, 5 - ); - assertEq(beforeOrAtIndex, 3); - assertEq(beforeOrAtDrawId, 4); - assertEq(afterOrAtIndex, 4); - assertEq(afterOrAtDrawId, 5); - } - - function testBinarySearchTargetBetween() public { - fillDrawRingBuffer([2, 4, 5, 6, 7]); - (uint16 beforeOrAtIndex, uint16 beforeOrAtDrawId, uint16 afterOrAtIndex, uint16 afterOrAtDrawId) = wrapper.binarySearch( - 0, 4, 5, 3 - ); - assertEq(beforeOrAtIndex, 0); - assertEq(beforeOrAtDrawId, 2); - assertEq(afterOrAtIndex, 1); - assertEq(afterOrAtDrawId, 4); - } - - function fillDrawRingBuffer(uint8[5] memory values) internal { - for (uint16 i = 0; i < values.length; i++) { - wrapper.setDrawRingBuffer(i, values[i]); - } - wrapper.setRingBufferInfo(uint16(values.length), uint16(values.length)); - } - - function add(uint16 drawId) internal { - wrapper.add(10000, drawId, alpha); - } - - function getDisbursedBetween(uint16 _startDrawId, uint16 _endDrawId) internal view returns (uint256) { - return wrapper.getDisbursedBetween(_startDrawId, _endDrawId, alpha); - } - + assertEq(getDisbursedBetween(3, 4), 2538); + } + + function testIntegrateInf() public { + assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 0, 100), 100); + assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 1, 100), 90); + assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 2, 100), 81); + assertEq(DrawAccumulatorLib.integrateInf(sd(0.9e18), 3, 100), 72); + } + + function testIntegrate() public { + assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 0, 1, 10000), 1000); + assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 1, 2, 10000), 899); + assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 2, 3, 10000), 809); + assertEq(DrawAccumulatorLib.integrate(sd(0.9e18), 3, 4, 10000), 728); + } + + function testBinarySearchTwoWithFirstMatchingTarget() public { + fillDrawRingBuffer([1, 3, 0, 0, 0]); + ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) = wrapper.binarySearch(0, 2, 2, 1); + assertEq(beforeOrAtIndex, 0); + assertEq(beforeOrAtDrawId, 1); + assertEq(afterOrAtIndex, 1); + assertEq(afterOrAtDrawId, 3); + } + + function testBinarySearchMatchingTarget() public { + fillDrawRingBuffer([1, 2, 3, 4, 5]); + ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) = wrapper.binarySearch(0, 4, 5, 3); + assertEq(beforeOrAtIndex, 2); + assertEq(beforeOrAtDrawId, 3); + assertEq(afterOrAtIndex, 3); + assertEq(afterOrAtDrawId, 4); + } + + function testBinarySearchFirstMatchingTarget() public { + fillDrawRingBuffer([1, 2, 3, 4, 5]); + ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) = wrapper.binarySearch(0, 4, 5, 1); + assertEq(beforeOrAtIndex, 0); + assertEq(beforeOrAtDrawId, 1); + assertEq(afterOrAtIndex, 1); + assertEq(afterOrAtDrawId, 2); + } + + function testBinarySearchLastMatchingTarget() public { + fillDrawRingBuffer([1, 2, 3, 4, 5]); + ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) = wrapper.binarySearch(0, 4, 5, 5); + assertEq(beforeOrAtIndex, 3); + assertEq(beforeOrAtDrawId, 4); + assertEq(afterOrAtIndex, 4); + assertEq(afterOrAtDrawId, 5); + } + + function testBinarySearchTargetBetween() public { + fillDrawRingBuffer([2, 4, 5, 6, 7]); + ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) = wrapper.binarySearch(0, 4, 5, 3); + assertEq(beforeOrAtIndex, 0); + assertEq(beforeOrAtDrawId, 2); + assertEq(afterOrAtIndex, 1); + assertEq(afterOrAtDrawId, 4); + } + + function fillDrawRingBuffer(uint8[5] memory values) internal { + for (uint16 i = 0; i < values.length; i++) { + wrapper.setDrawRingBuffer(i, values[i]); + } + wrapper.setRingBufferInfo(uint16(values.length), uint16(values.length)); + } + + function add(uint16 drawId) internal { + wrapper.add(10000, drawId, alpha); + } + + function getDisbursedBetween( + uint16 _startDrawId, + uint16 _endDrawId + ) internal view returns (uint256) { + return wrapper.getDisbursedBetween(_startDrawId, _endDrawId, alpha); + } } diff --git a/test/libraries/TierCalculationLib.t.sol b/test/libraries/TierCalculationLib.t.sol index a9a3316..eb5da1b 100644 --- a/test/libraries/TierCalculationLib.t.sol +++ b/test/libraries/TierCalculationLib.t.sol @@ -9,134 +9,148 @@ import { SD59x18, sd, unwrap, toSD59x18 } from "prb-math/SD59x18.sol"; import { UD60x18, ud } from "prb-math/UD60x18.sol"; contract TierCalculationLibTest is Test { - - TierCalculationLibWrapper wrapper; - - function setUp() public { - wrapper = new TierCalculationLibWrapper(); - } - - function testGetTierOdds() public { - assertEq(unwrap(TierCalculationLib.getTierOdds(0, 4, 365)), 2739726027397260); - assertEq(unwrap(TierCalculationLib.getTierOdds(1, 4, 365)), 19579642462506911); - assertEq(unwrap(TierCalculationLib.getTierOdds(2, 4, 365)), 139927275620255366); - assertEq(unwrap(TierCalculationLib.getTierOdds(3, 4, 365)), 1e18); - } - - function testEstimatePrizeFrequencyInDraws() public { - assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(0, 4, 365), 366); - assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(1, 4, 365), 52); - assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(2, 4, 365), 8); - assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(3, 4, 365), 1); - } - - function testPrizeCount() public { - assertEq(TierCalculationLib.prizeCount(0), 1); - } - - function testCalculateWinningZoneWithTierOdds() public { - assertEq(TierCalculationLib.calculateWinningZone(1000, sd(0.333e18), sd(1e18)), 333); - } - - function testCalculateWinningZoneWithVaultPortion() public { - assertEq(TierCalculationLib.calculateWinningZone(1000, sd(1e18), sd(0.444e18)), 444); - } - - function testCalculateWinningZoneWithPrizeCount() public { - assertEq(TierCalculationLib.calculateWinningZone(1000, sd(1e18), sd(1e18)), 1000); - } - - function testEstimatedClaimCount() public { - // 2: 4.002739726 - // 3: 16.2121093 - // 4: 66.31989471 - // 5: 271.5303328 - // 6: 1109.21076 - // 7: 4518.562795 - // 8: 18359.91762 - // 9: 74437.0802 - // 10: 301242.1839 - // 11: 1217269.1 - // 12: 4912623.73 - // 13: 19805539.61 - // 14: 79777192.14 - // 15: 321105957.4 - // 16: 1291645055 - - assertEq(wrapper.estimatedClaimCount(2, 365), 4); - assertEq(wrapper.estimatedClaimCount(3, 365), 16); - assertEq(wrapper.estimatedClaimCount(4, 365), 66); - assertEq(wrapper.estimatedClaimCount(5, 365), 270); - assertEq(wrapper.estimatedClaimCount(6, 365), 1108); - assertEq(wrapper.estimatedClaimCount(7, 365), 4517); - assertEq(wrapper.estimatedClaimCount(8, 365), 18358); - assertEq(wrapper.estimatedClaimCount(9, 365), 74435); - assertEq(wrapper.estimatedClaimCount(10, 365), 301239); - assertEq(wrapper.estimatedClaimCount(11, 365), 1217266); - assertEq(wrapper.estimatedClaimCount(12, 365), 4912619); - assertEq(wrapper.estimatedClaimCount(13, 365), 19805536); - assertEq(wrapper.estimatedClaimCount(14, 365), 79777187); - assertEq(wrapper.estimatedClaimCount(15, 365), 321105952); - assertEq(wrapper.estimatedClaimCount(16, 365), 1291645048); - } - - function testCanaryPrizeCount() public { - assertEq(wrapper.canaryPrizeCount(2, 10, 0, 100).unwrap(), 2361904761904761904); - assertEq(wrapper.canaryPrizeCount(3, 10, 0, 100).unwrap(), 8464516129032258048); - assertEq(wrapper.canaryPrizeCount(4, 10, 0, 100).unwrap(), 31843902439024390144); - assertEq(wrapper.canaryPrizeCount(5, 10, 0, 100).unwrap(), 122478431372549018624); - assertEq(wrapper.canaryPrizeCount(6, 10, 0, 100).unwrap(), 476747540983606554624); - assertEq(wrapper.canaryPrizeCount(7, 10, 0, 100).unwrap(), 1869160563380281688064); - assertEq(wrapper.canaryPrizeCount(8, 10, 0, 100).unwrap(), 7362686419753086418944); - assertEq(wrapper.canaryPrizeCount(9, 10, 0, 100).unwrap(), 29095103296703296700416); - assertEq(wrapper.canaryPrizeCount(10, 10, 0, 100).unwrap(), 115239540594059404902400); - assertEq(wrapper.canaryPrizeCount(11, 10, 0, 100).unwrap(), 457216922522522522484736); - assertEq(wrapper.canaryPrizeCount(12, 10, 0, 100).unwrap(), 1816376277685950406983680); - assertEq(wrapper.canaryPrizeCount(13, 10, 0, 100).unwrap(), 7223167804580152605671424); - assertEq(wrapper.canaryPrizeCount(14, 10, 0, 100).unwrap(), 28747343160283687758594048); - assertEq(wrapper.canaryPrizeCount(15, 10, 0, 100).unwrap(), 114485055406622515774095360); - } - - function testIsWinner_WinsAll() external { - uint8 tier = 5; - uint8 numberOfTiers = 6; - vm.assume(tier < numberOfTiers); - uint16 grandPrizePeriod = 365; - SD59x18 tierOdds = TierCalculationLib.getTierOdds(tier, numberOfTiers, grandPrizePeriod); - // console2.log("tierOdds", SD59x18.unwrap(tierOdds)); - uint32 prizeCount = uint32(TierCalculationLib.prizeCount(tier)); - SD59x18 vaultContribution = toSD59x18(int256(1)); - // console2.log("vaultContribution", SD59x18.unwrap(vaultContribution)); - - uint wins; - for (uint i = 0; i < prizeCount; i++) { - if (TierCalculationLib.isWinner(uint256(keccak256(abi.encode(i))), 1000, 1000, vaultContribution, tierOdds)) { - wins++; - } - } - - assertApproxEqAbs(wins, prizeCount, 0); + TierCalculationLibWrapper wrapper; + + function setUp() public { + wrapper = new TierCalculationLibWrapper(); + } + + function testGetTierOdds() public { + assertEq(unwrap(TierCalculationLib.getTierOdds(0, 4, 365)), 2739726027397260); + assertEq(unwrap(TierCalculationLib.getTierOdds(1, 4, 365)), 19579642462506911); + assertEq(unwrap(TierCalculationLib.getTierOdds(2, 4, 365)), 139927275620255366); + assertEq(unwrap(TierCalculationLib.getTierOdds(3, 4, 365)), 1e18); + } + + function testEstimatePrizeFrequencyInDraws() public { + assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(0, 4, 365), 366); + assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(1, 4, 365), 52); + assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(2, 4, 365), 8); + assertEq(TierCalculationLib.estimatePrizeFrequencyInDraws(3, 4, 365), 1); + } + + function testPrizeCount() public { + assertEq(TierCalculationLib.prizeCount(0), 1); + } + + function testCalculateWinningZoneWithTierOdds() public { + assertEq(TierCalculationLib.calculateWinningZone(1000, sd(0.333e18), sd(1e18)), 333); + } + + function testCalculateWinningZoneWithVaultPortion() public { + assertEq(TierCalculationLib.calculateWinningZone(1000, sd(1e18), sd(0.444e18)), 444); + } + + function testCalculateWinningZoneWithPrizeCount() public { + assertEq(TierCalculationLib.calculateWinningZone(1000, sd(1e18), sd(1e18)), 1000); + } + + function testEstimatedClaimCount() public { + // 2: 4.002739726 + // 3: 16.2121093 + // 4: 66.31989471 + // 5: 271.5303328 + // 6: 1109.21076 + // 7: 4518.562795 + // 8: 18359.91762 + // 9: 74437.0802 + // 10: 301242.1839 + // 11: 1217269.1 + // 12: 4912623.73 + // 13: 19805539.61 + // 14: 79777192.14 + // 15: 321105957.4 + // 16: 1291645055 + + assertEq(wrapper.estimatedClaimCount(2, 365), 4); + assertEq(wrapper.estimatedClaimCount(3, 365), 16); + assertEq(wrapper.estimatedClaimCount(4, 365), 66); + assertEq(wrapper.estimatedClaimCount(5, 365), 270); + assertEq(wrapper.estimatedClaimCount(6, 365), 1108); + assertEq(wrapper.estimatedClaimCount(7, 365), 4517); + assertEq(wrapper.estimatedClaimCount(8, 365), 18358); + assertEq(wrapper.estimatedClaimCount(9, 365), 74435); + assertEq(wrapper.estimatedClaimCount(10, 365), 301239); + assertEq(wrapper.estimatedClaimCount(11, 365), 1217266); + assertEq(wrapper.estimatedClaimCount(12, 365), 4912619); + assertEq(wrapper.estimatedClaimCount(13, 365), 19805536); + assertEq(wrapper.estimatedClaimCount(14, 365), 79777187); + assertEq(wrapper.estimatedClaimCount(15, 365), 321105952); + assertEq(wrapper.estimatedClaimCount(16, 365), 1291645048); + } + + function testCanaryPrizeCount() public { + assertEq(wrapper.canaryPrizeCount(2, 10, 0, 100).unwrap(), 2361904761904761904); + assertEq(wrapper.canaryPrizeCount(3, 10, 0, 100).unwrap(), 8464516129032258048); + assertEq(wrapper.canaryPrizeCount(4, 10, 0, 100).unwrap(), 31843902439024390144); + assertEq(wrapper.canaryPrizeCount(5, 10, 0, 100).unwrap(), 122478431372549018624); + assertEq(wrapper.canaryPrizeCount(6, 10, 0, 100).unwrap(), 476747540983606554624); + assertEq(wrapper.canaryPrizeCount(7, 10, 0, 100).unwrap(), 1869160563380281688064); + assertEq(wrapper.canaryPrizeCount(8, 10, 0, 100).unwrap(), 7362686419753086418944); + assertEq(wrapper.canaryPrizeCount(9, 10, 0, 100).unwrap(), 29095103296703296700416); + assertEq(wrapper.canaryPrizeCount(10, 10, 0, 100).unwrap(), 115239540594059404902400); + assertEq(wrapper.canaryPrizeCount(11, 10, 0, 100).unwrap(), 457216922522522522484736); + assertEq(wrapper.canaryPrizeCount(12, 10, 0, 100).unwrap(), 1816376277685950406983680); + assertEq(wrapper.canaryPrizeCount(13, 10, 0, 100).unwrap(), 7223167804580152605671424); + assertEq(wrapper.canaryPrizeCount(14, 10, 0, 100).unwrap(), 28747343160283687758594048); + assertEq(wrapper.canaryPrizeCount(15, 10, 0, 100).unwrap(), 114485055406622515774095360); + } + + function testIsWinner_WinsAll() external { + uint8 tier = 5; + uint8 numberOfTiers = 6; + vm.assume(tier < numberOfTiers); + uint16 grandPrizePeriod = 365; + SD59x18 tierOdds = TierCalculationLib.getTierOdds(tier, numberOfTiers, grandPrizePeriod); + // console2.log("tierOdds", SD59x18.unwrap(tierOdds)); + uint32 prizeCount = uint32(TierCalculationLib.prizeCount(tier)); + SD59x18 vaultContribution = toSD59x18(int256(1)); + // console2.log("vaultContribution", SD59x18.unwrap(vaultContribution)); + + uint wins; + for (uint i = 0; i < prizeCount; i++) { + if ( + TierCalculationLib.isWinner( + uint256(keccak256(abi.encode(i))), + 1000, + 1000, + vaultContribution, + tierOdds + ) + ) { + wins++; + } } - function testIsWinner_HalfLiquidity() external { - uint8 tier = 5; - uint8 numberOfTiers = 6; - vm.assume(tier < numberOfTiers); - uint16 grandPrizePeriod = 365; - SD59x18 tierOdds = TierCalculationLib.getTierOdds(tier, numberOfTiers, grandPrizePeriod); - // console2.log("tierOdds", SD59x18.unwrap(tierOdds)); - uint32 prizeCount = uint32(TierCalculationLib.prizeCount(tier)); - SD59x18 vaultContribution = toSD59x18(int256(1)); - // console2.log("vaultContribution", SD59x18.unwrap(vaultContribution)); - - uint wins; - for (uint i = 0; i < prizeCount; i++) { - if (TierCalculationLib.isWinner(uint256(keccak256(abi.encode(i))), 500, 1000, vaultContribution, tierOdds)) { - wins++; - } - } - - assertApproxEqAbs(wins, prizeCount/2, 20); + assertApproxEqAbs(wins, prizeCount, 0); + } + + function testIsWinner_HalfLiquidity() external { + uint8 tier = 5; + uint8 numberOfTiers = 6; + vm.assume(tier < numberOfTiers); + uint16 grandPrizePeriod = 365; + SD59x18 tierOdds = TierCalculationLib.getTierOdds(tier, numberOfTiers, grandPrizePeriod); + // console2.log("tierOdds", SD59x18.unwrap(tierOdds)); + uint32 prizeCount = uint32(TierCalculationLib.prizeCount(tier)); + SD59x18 vaultContribution = toSD59x18(int256(1)); + // console2.log("vaultContribution", SD59x18.unwrap(vaultContribution)); + + uint wins; + for (uint i = 0; i < prizeCount; i++) { + if ( + TierCalculationLib.isWinner( + uint256(keccak256(abi.encode(i))), + 500, + 1000, + vaultContribution, + tierOdds + ) + ) { + wins++; + } } + assertApproxEqAbs(wins, prizeCount / 2, 20); + } } diff --git a/test/libraries/UD34x4.t.sol b/test/libraries/UD34x4.t.sol index 0c541c3..5f77ae3 100644 --- a/test/libraries/UD34x4.t.sol +++ b/test/libraries/UD34x4.t.sol @@ -4,53 +4,43 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; import { UD60x18, toUD60x18 } from "prb-math/UD60x18.sol"; -import { - UD34x4, - toUD34x4, - fromUD34x4, - intoUD60x18, - fromUD60x18, - uUNIT, - PRBMath_UD34x4_Convert_Overflow, - PRBMath_UD34x4_fromUD60x18_Convert_Overflow, - uMAX_UD34x4 -} from "src/libraries/UD34x4.sol"; +import { UD34x4, toUD34x4, fromUD34x4, intoUD60x18, fromUD60x18, uUNIT, PRBMath_UD34x4_Convert_Overflow, PRBMath_UD34x4_fromUD60x18_Convert_Overflow, uMAX_UD34x4 } from "src/libraries/UD34x4.sol"; contract UD34x4Test is Test { - - function testToUD34x4_withUintMax() public { - uint128 legalMax = type(uint128).max / uUNIT; - UD34x4 result = toUD34x4(legalMax); - assertEq(UD34x4.unwrap(result), legalMax * uUNIT); - } - - function testToUD34x4_overflow() public { - uint128 legalMax = uMAX_UD34x4 / uUNIT; - vm.expectRevert(abi.encodeWithSelector(PRBMath_UD34x4_Convert_Overflow.selector, legalMax+1)); - toUD34x4(legalMax+1); - } - - function testIntoUD60x18() public { - UD34x4 x = UD34x4.wrap(100e4); - UD60x18 result = intoUD60x18(x); - assertEq(result.unwrap(), 100e18); - } - - function testIntoUD60x18_large() public pure { - UD34x4 x = UD34x4.wrap(6004291579826925202373984590); - intoUD60x18(x); - } - - function testFromUD60x18_normal() public { - UD60x18 x = UD60x18.wrap(100.1234e18); - UD34x4 result = fromUD60x18(x); - assertEq(UD34x4.unwrap(result), 100.1234e4); - } - - function testFromUD60x18_overflow() public { - UD60x18 x = toUD60x18(uMAX_UD34x4); - vm.expectRevert(abi.encodeWithSelector(PRBMath_UD34x4_fromUD60x18_Convert_Overflow.selector, x.unwrap())); - fromUD60x18(x); - } - + function testToUD34x4_withUintMax() public { + uint128 legalMax = type(uint128).max / uUNIT; + UD34x4 result = toUD34x4(legalMax); + assertEq(UD34x4.unwrap(result), legalMax * uUNIT); + } + + function testToUD34x4_overflow() public { + uint128 legalMax = uMAX_UD34x4 / uUNIT; + vm.expectRevert(abi.encodeWithSelector(PRBMath_UD34x4_Convert_Overflow.selector, legalMax + 1)); + toUD34x4(legalMax + 1); + } + + function testIntoUD60x18() public { + UD34x4 x = UD34x4.wrap(100e4); + UD60x18 result = intoUD60x18(x); + assertEq(result.unwrap(), 100e18); + } + + function testIntoUD60x18_large() public pure { + UD34x4 x = UD34x4.wrap(6004291579826925202373984590); + intoUD60x18(x); + } + + function testFromUD60x18_normal() public { + UD60x18 x = UD60x18.wrap(100.1234e18); + UD34x4 result = fromUD60x18(x); + assertEq(UD34x4.unwrap(result), 100.1234e4); + } + + function testFromUD60x18_overflow() public { + UD60x18 x = toUD60x18(uMAX_UD34x4); + vm.expectRevert( + abi.encodeWithSelector(PRBMath_UD34x4_fromUD60x18_Convert_Overflow.selector, x.unwrap()) + ); + fromUD60x18(x); + } } diff --git a/test/mocks/ERC20Mintable.sol b/test/mocks/ERC20Mintable.sol index e203817..fcd9bcc 100644 --- a/test/mocks/ERC20Mintable.sol +++ b/test/mocks/ERC20Mintable.sol @@ -4,10 +4,7 @@ pragma solidity 0.8.17; import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; contract ERC20Mintable is ERC20 { - constructor ( - string memory _name, - string memory _symbol - ) ERC20(_name, _symbol) {} + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} function mint(address _account, uint256 _amount) public { _mint(_account, _amount); diff --git a/test/wrappers/BitLibWrapper.sol b/test/wrappers/BitLibWrapper.sol index b360a84..9505a95 100644 --- a/test/wrappers/BitLibWrapper.sol +++ b/test/wrappers/BitLibWrapper.sol @@ -6,24 +6,21 @@ import { BitLib } from "src/libraries/BitLib.sol"; // Note: Need to store the results from the library in a variable to be picked up by forge coverage // See: https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086 contract BitLibWrapper { + /// @notice Flips one bit in a packed array of bits. + /// @param packedBits The bit storage. There are 256 bits in a uint256. + /// @param bit The bit to flip + /// @return The passed bit storage that has the desired bit flipped. + function flipBit(uint256 packedBits, uint8 bit) external pure returns (uint256) { + uint256 result = BitLib.flipBit(packedBits, bit); + return result; + } - - /// @notice Flips one bit in a packed array of bits. - /// @param packedBits The bit storage. There are 256 bits in a uint256. - /// @param bit The bit to flip - /// @return The passed bit storage that has the desired bit flipped. - function flipBit(uint256 packedBits, uint8 bit) external pure returns (uint256) { - uint256 result = BitLib.flipBit(packedBits, bit); - return result; - } - - /// @notice Retrieves the value of one bit from a packed array of bits. - /// @param packedBits The bit storage. There are 256 bits in a uint256. - /// @param bit The bit to retrieve - /// @return The value of the desired bit - function getBit(uint256 packedBits, uint8 bit) external pure returns (bool) { - bool result = BitLib.getBit(packedBits, bit); - return result; - } - + /// @notice Retrieves the value of one bit from a packed array of bits. + /// @param packedBits The bit storage. There are 256 bits in a uint256. + /// @param bit The bit to retrieve + /// @return The value of the desired bit + function getBit(uint256 packedBits, uint8 bit) external pure returns (bool) { + bool result = BitLib.getBit(packedBits, bit); + return result; + } } diff --git a/test/wrappers/DrawAccumulatorLibWrapper.sol b/test/wrappers/DrawAccumulatorLibWrapper.sol index 1d84e35..2e4b89a 100644 --- a/test/wrappers/DrawAccumulatorLibWrapper.sol +++ b/test/wrappers/DrawAccumulatorLibWrapper.sol @@ -11,81 +11,99 @@ import { E, SD59x18, sd, unwrap, toSD59x18, fromSD59x18 } from "prb-math/SD59x18 // Note: Need to store the results from the library in a variable to be picked up by forge coverage // See: https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086 contract DrawAccumulatorLibWrapper { + DrawAccumulatorLib.Accumulator public accumulator; - DrawAccumulatorLib.Accumulator public accumulator; + function setDrawRingBuffer(uint16 index, uint8 value) public { + accumulator.drawRingBuffer[index] = value; + } - function setDrawRingBuffer(uint16 index, uint8 value) public { - accumulator.drawRingBuffer[index] = value; - } + function setRingBufferInfo(uint16 nextIndex, uint16 cardinality) public { + accumulator.ringBufferInfo.cardinality = cardinality; + accumulator.ringBufferInfo.nextIndex = nextIndex; + } - function setRingBufferInfo(uint16 nextIndex, uint16 cardinality) public { - accumulator.ringBufferInfo.cardinality = cardinality; - accumulator.ringBufferInfo.nextIndex = nextIndex; - } + function add(uint256 _amount, uint16 _drawId, SD59x18 _alpha) public returns (bool) { + bool result = DrawAccumulatorLib.add(accumulator, _amount, _drawId, _alpha); + return result; + } - function add(uint256 _amount, uint16 _drawId, SD59x18 _alpha) public returns (bool) { - bool result = DrawAccumulatorLib.add(accumulator, _amount, _drawId, _alpha); - return result; - } + function getTotalRemaining(uint16 _endDrawId, SD59x18 _alpha) public view returns (uint256) { + uint256 result = DrawAccumulatorLib.getTotalRemaining(accumulator, _endDrawId, _alpha); + return result; + } - function getTotalRemaining(uint16 _endDrawId, SD59x18 _alpha) public view returns (uint256) { - uint256 result = DrawAccumulatorLib.getTotalRemaining(accumulator, _endDrawId, _alpha); - return result; - } + function newestObservation() public view returns (Observation memory) { + Observation memory result = DrawAccumulatorLib.newestObservation(accumulator); + return result; + } - function newestObservation() public view returns (Observation memory) { - Observation memory result = DrawAccumulatorLib.newestObservation(accumulator); - return result; - } + /** + * Requires endDrawId to be greater than (the newest draw id - 1) + */ + function getDisbursedBetween( + uint16 _startDrawId, + uint16 _endDrawId, + SD59x18 _alpha + ) public view returns (uint256) { + uint256 result = DrawAccumulatorLib.getDisbursedBetween( + accumulator, + _startDrawId, + _endDrawId, + _alpha + ); + return result; + } - /** - * Requires endDrawId to be greater than (the newest draw id - 1) - */ - function getDisbursedBetween( - uint16 _startDrawId, - uint16 _endDrawId, - SD59x18 _alpha - ) public view returns (uint256) { - uint256 result = DrawAccumulatorLib.getDisbursedBetween(accumulator, _startDrawId, _endDrawId, _alpha); - return result; - } + /** + * @notice Returns the remaining prize tokens available from relative draw _x + */ + function integrateInf(SD59x18 _alpha, uint _x, uint _k) public pure returns (uint256) { + uint256 result = DrawAccumulatorLib.integrateInf(_alpha, _x, _k); + return result; + } - /** - * @notice Returns the remaining prize tokens available from relative draw _x - */ - function integrateInf(SD59x18 _alpha, uint _x, uint _k) public pure returns (uint256) { - uint256 result = DrawAccumulatorLib.integrateInf(_alpha, _x, _k); - return result; - } + /** + * @notice returns the number of tokens that were given out between draw _start and draw _end + */ + function integrate( + SD59x18 _alpha, + uint _start, + uint _end, + uint _k + ) public pure returns (uint256) { + uint256 result = DrawAccumulatorLib.integrate(_alpha, _start, _end, _k); + return result; + } - /** - * @notice returns the number of tokens that were given out between draw _start and draw _end - */ - function integrate(SD59x18 _alpha, uint _start, uint _end, uint _k) public pure returns (uint256) { - uint256 result = DrawAccumulatorLib.integrate(_alpha, _start, _end, _k); - return result; - } + function computeC(SD59x18 _alpha, uint _x, uint _k) public pure returns (SD59x18) { + SD59x18 result = DrawAccumulatorLib.computeC(_alpha, _x, _k); + return result; + } - function computeC(SD59x18 _alpha, uint _x, uint _k) public pure returns (SD59x18) { - SD59x18 result = DrawAccumulatorLib.computeC(_alpha, _x, _k); - return result; - } - - /** - */ - function binarySearch( - uint16 _oldestIndex, - uint16 _newestIndex, - uint16 _cardinality, - uint16 _targetLastCompletedDrawId - ) public view returns ( - uint16 beforeOrAtIndex, - uint16 beforeOrAtDrawId, - uint16 afterOrAtIndex, - uint16 afterOrAtDrawId - ) { - (beforeOrAtIndex, beforeOrAtDrawId, afterOrAtIndex, afterOrAtDrawId) = DrawAccumulatorLib.binarySearch( - accumulator.drawRingBuffer, _oldestIndex, _newestIndex, _cardinality, _targetLastCompletedDrawId - ); - } + /** + */ + function binarySearch( + uint16 _oldestIndex, + uint16 _newestIndex, + uint16 _cardinality, + uint16 _targetLastCompletedDrawId + ) + public + view + returns ( + uint16 beforeOrAtIndex, + uint16 beforeOrAtDrawId, + uint16 afterOrAtIndex, + uint16 afterOrAtDrawId + ) + { + (beforeOrAtIndex, beforeOrAtDrawId, afterOrAtIndex, afterOrAtDrawId) = DrawAccumulatorLib + .binarySearch( + accumulator.drawRingBuffer, + _oldestIndex, + _newestIndex, + _cardinality, + _targetLastCompletedDrawId + ); + } } diff --git a/test/wrappers/TierCalculationLibWrapper.sol b/test/wrappers/TierCalculationLibWrapper.sol index f5582d8..d16baa4 100644 --- a/test/wrappers/TierCalculationLibWrapper.sol +++ b/test/wrappers/TierCalculationLibWrapper.sol @@ -7,16 +7,26 @@ import { UD60x18 } from "prb-math/UD60x18.sol"; // Note: Need to store the results from the library in a variable to be picked up by forge coverage // See: https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086 contract TierCalculationLibWrapper { + function canaryPrizeCount( + uint8 _numberOfTiers, + uint8 _canaryShares, + uint8 _reserveShares, + uint8 _tierShares + ) external pure returns (UD60x18) { + UD60x18 result = TierCalculationLib.canaryPrizeCount( + _numberOfTiers, + _canaryShares, + _reserveShares, + _tierShares + ); + return result; + } - function canaryPrizeCount( - uint8 _numberOfTiers, uint8 _canaryShares, uint8 _reserveShares, uint8 _tierShares - ) external pure returns (UD60x18) { - UD60x18 result = TierCalculationLib.canaryPrizeCount(_numberOfTiers, _canaryShares, _reserveShares, _tierShares); - return result; - } - - function estimatedClaimCount(uint8 _numberOfTiers, uint16 _grandPrizePeriod) external pure returns (uint32) { - uint32 result = TierCalculationLib.estimatedClaimCount(_numberOfTiers, _grandPrizePeriod); - return result; - } + function estimatedClaimCount( + uint8 _numberOfTiers, + uint16 _grandPrizePeriod + ) external pure returns (uint32) { + uint32 result = TierCalculationLib.estimatedClaimCount(_numberOfTiers, _grandPrizePeriod); + return result; + } }