Skip to content

Commit

Permalink
Merge pull request #112 from GenerationSoftware/shutdown-obs-freeze
Browse files Browse the repository at this point in the history
Freeze observations after shutdown
  • Loading branch information
asselstine committed Apr 17, 2024
2 parents 9e35c45 + 060de4a commit f26119f
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 34 deletions.
72 changes: 58 additions & 14 deletions src/PrizePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SD59x18, convert, sd } from "prb-math/SD59x18.sol";
import { SD1x18, unwrap, UNIT } from "prb-math/SD1x18.sol";
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";

import { DrawAccumulatorLib, Observation } from "./libraries/DrawAccumulatorLib.sol";
import { DrawAccumulatorLib, Observation, MAX_OBSERVATION_CARDINALITY } from "./libraries/DrawAccumulatorLib.sol";
import { TieredLiquidityDistributor, Tier } from "./abstract/TieredLiquidityDistributor.sol";
import { TierCalculationLib } from "./libraries/TierCalculationLib.sol";

Expand Down Expand Up @@ -120,6 +120,11 @@ error OnlyCreator();
/// @notice Thrown when the draw manager has already been set
error DrawManagerAlreadySet();

/// @notice Thrown when the grand prize period is too large
/// @param grandPrizePeriodDraws The set grand prize period
/// @param maxGrandPrizePeriodDraws The max grand prize period
error GrandPrizePeriodDrawsTooLarge(uint24 grandPrizePeriodDraws, uint24 maxGrandPrizePeriodDraws);

/// @notice Constructor Parameters
/// @param prizeToken The token to use for prizes
/// @param twabController The Twab Controller to retrieve time-weighted average balances from
Expand Down Expand Up @@ -342,6 +347,10 @@ contract PrizePool is TieredLiquidityDistributor {
revert FirstDrawOpensInPast();
}

if (params.grandPrizePeriodDraws >= MAX_OBSERVATION_CARDINALITY) {
revert GrandPrizePeriodDrawsTooLarge(params.grandPrizePeriodDraws, MAX_OBSERVATION_CARDINALITY - 1);
}

uint48 twabPeriodOffset = params.twabController.PERIOD_OFFSET();
uint48 twabPeriodLength = params.twabController.PERIOD_LENGTH();

Expand Down Expand Up @@ -405,10 +414,12 @@ contract PrizePool is TieredLiquidityDistributor {
if (_deltaBalance < _amount) {
revert ContributionGTDeltaBalance(_amount, _deltaBalance);
}
uint24 openDrawId_ = getOpenDrawId();
_vaultAccumulator[_prizeVault].add(_amount, openDrawId_);
_totalAccumulator.add(_amount, openDrawId_);
emit ContributePrizeTokens(_prizeVault, openDrawId_, _amount);

uint24 _openDrawId = getOpenDrawId();
_vaultAccumulator[_prizeVault].add(_amount, _openDrawId);
_totalAccumulator.add(_amount, _openDrawId);

emit ContributePrizeTokens(_prizeVault, _openDrawId, _amount);
return _deltaBalance;
}

Expand Down Expand Up @@ -665,6 +676,19 @@ contract PrizePool is TieredLiquidityDistributor {
);
}

/// @notice Returns the newest observation for the total accumulator
/// @return The newest observation
function getTotalAccumulatorNewestObservation() external view returns (Observation memory) {
return _totalAccumulator.newestObservation();
}

/// @notice Returns the newest observation for the specified vault accumulator
/// @param _vault The vault to check
/// @return The newest observation for the vault
function getVaultAccumulatorNewestObservation(address _vault) external view returns (Observation memory) {
return _vaultAccumulator[_vault].newestObservation();
}

/// @notice Computes the expected duration prize accrual for a tier.
/// @param _tier The tier to check
/// @return The number of draws
Expand Down Expand Up @@ -711,6 +735,23 @@ contract PrizePool is TieredLiquidityDistributor {
return _claimedPrizes[_vault][_winner][_lastAwardedDrawId][_tier][_prizeIndex];
}

/// @notice Returns whether the winner has claimed the tier for the specified draw
/// @param _vault The vault to check
/// @param _winner The account to check
/// @param _drawId The draw ID to check
/// @param _tier The tier to check
/// @param _prizeIndex The prize index to check
/// @return True if the winner claimed the tier for the specified draw, false otherwise.
function wasClaimed(
address _vault,
address _winner,
uint24 _drawId,
uint8 _tier,
uint32 _prizeIndex
) external view returns (bool) {
return _claimedPrizes[_vault][_winner][_drawId][_tier][_prizeIndex];
}

/// @notice Returns the balance of rewards earned for the given address.
/// @param _recipient The recipient to retrieve the reward balance for
/// @return The balance of rewards for the given recipient
Expand Down Expand Up @@ -757,9 +798,12 @@ contract PrizePool is TieredLiquidityDistributor {
/// going to the inaccessible draw zero.
/// @dev First draw has an ID of `1`. This means that if `_lastAwardedDrawId` is zero,
/// we know that no draws have been awarded yet.
/// @dev Capped at the shutdown draw ID if the prize pool has shutdown.
/// @return The ID of the draw period that the current block is in
function getOpenDrawId() public view returns (uint24) {
return getDrawId(block.timestamp);
uint24 shutdownDrawId = getShutdownDrawId();
uint24 openDrawId = getDrawId(block.timestamp);
return openDrawId > shutdownDrawId ? shutdownDrawId : openDrawId;
}

/// @notice Returns the open draw id for the given timestamp
Expand Down Expand Up @@ -829,20 +873,20 @@ contract PrizePool is TieredLiquidityDistributor {
/// @param _account The account whose vault twab is measured
/// @return The portion of the shutdown balance that the account is entitled to.
function computeShutdownPortion(address _vault, address _account) public view returns (ShutdownPortion memory) {
uint24 shutdownDrawId = getDrawIdPriorToShutdown();
uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(shutdownDrawId, grandPrizePeriodDraws);
uint24 drawIdPriorToShutdown = getShutdownDrawId() - 1;
uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(drawIdPriorToShutdown, grandPrizePeriodDraws);

(uint256 vaultContrib, uint256 totalContrib) = _getVaultShares(
_vault,
startDrawIdInclusive,
shutdownDrawId
drawIdPriorToShutdown
);

(uint256 _userTwab, uint256 _vaultTwabTotalSupply) = getVaultUserBalanceAndTotalSupplyTwab(
_vault,
_account,
startDrawIdInclusive,
shutdownDrawId
drawIdPriorToShutdown
);

if (_vaultTwabTotalSupply == 0) {
Expand Down Expand Up @@ -870,9 +914,9 @@ contract PrizePool is TieredLiquidityDistributor {

// if we haven't withdrawn yet, add the portion of the shutdown balance
if ((withdrawalObservation.available + withdrawalObservation.disbursed) == 0) {
(balance, withdrawalObservation) = getShutdownInfo();
shutdownPortion = computeShutdownPortion(_vault, _account);
_shutdownPortions[_vault][_account] = shutdownPortion;
(balance, withdrawalObservation) = getShutdownInfo();
} else {
shutdownPortion = _shutdownPortions[_vault][_account];
}
Expand Down Expand Up @@ -906,10 +950,10 @@ contract PrizePool is TieredLiquidityDistributor {
return balance;
}

/// @notice Returns the draw ID that will be awarded prior to the prize pool being shutdown
/// @notice Returns the open draw ID at the time of shutdown.
/// @return The draw id
function getDrawIdPriorToShutdown() public view returns (uint24) {
return getDrawId(shutdownAt()) - 1;
function getShutdownDrawId() public view returns (uint24) {
return getDrawId(shutdownAt());
}

/// @notice Returns the timestamp at which the prize pool will be considered inactive and shutdown
Expand Down
15 changes: 8 additions & 7 deletions src/libraries/DrawAccumulatorLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ pragma solidity ^0.8.24;
import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
import { RingBufferLib } from "ring-buffer-lib/RingBufferLib.sol";

// The maximum number of observations that can be recorded.
uint16 constant MAX_OBSERVATION_CARDINALITY = 366;

/// @notice Thrown when adding balance for draw zero.
error AddToDrawZero();

Expand Down Expand Up @@ -37,8 +40,6 @@ struct RingBufferInfo {
/// @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.
uint16 internal constant MAX_CARDINALITY = 366;

/// @notice An accumulator for a draw.
/// @param ringBufferInfo The metadata for the drawRingBuffer
Expand Down Expand Up @@ -70,7 +71,7 @@ library DrawAccumulatorLib {
RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo;

uint24 newestDrawId_ = accumulator.drawRingBuffer[
RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY)
RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_OBSERVATION_CARDINALITY)
];

if (_drawId < newestDrawId_) {
Expand All @@ -82,7 +83,7 @@ library DrawAccumulatorLib {
Observation memory newestObservation_ = accumulatorObservations[newestDrawId_];
if (_drawId != newestDrawId_) {
uint16 cardinality = ringBufferInfo.cardinality;
if (ringBufferInfo.cardinality < MAX_CARDINALITY) {
if (ringBufferInfo.cardinality < MAX_OBSERVATION_CARDINALITY) {
cardinality += 1;
} else {
// Delete the old observation to save gas (older than 1 year)
Expand All @@ -99,7 +100,7 @@ library DrawAccumulatorLib {
});

accumulator.ringBufferInfo = RingBufferInfo({
nextIndex: uint16(RingBufferLib.nextIndex(ringBufferInfo.nextIndex, MAX_CARDINALITY)),
nextIndex: uint16(RingBufferLib.nextIndex(ringBufferInfo.nextIndex, MAX_OBSERVATION_CARDINALITY)),
cardinality: cardinality
});

Expand All @@ -120,7 +121,7 @@ library DrawAccumulatorLib {
function newestDrawId(Accumulator storage accumulator) internal view returns (uint256) {
return
accumulator.drawRingBuffer[
RingBufferLib.newestIndex(accumulator.ringBufferInfo.nextIndex, MAX_CARDINALITY)
RingBufferLib.newestIndex(accumulator.ringBufferInfo.nextIndex, MAX_OBSERVATION_CARDINALITY)
];
}

Expand Down Expand Up @@ -157,7 +158,7 @@ library DrawAccumulatorLib {
RingBufferLib.oldestIndex(
ringBufferInfo.nextIndex,
ringBufferInfo.cardinality,
MAX_CARDINALITY
MAX_OBSERVATION_CARDINALITY
)
);
uint16 newestIndex = uint16(
Expand Down
79 changes: 66 additions & 13 deletions test/PrizePool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -732,26 +732,26 @@ contract PrizePoolTest is Test {
assertEq(prizePool.getTotalContributedBetween(1, 1), 100e18); // ensure not a single wei is lost!
}

function test_getDrawIdPriorToShutdown_init() public {
function test_getShutdownDrawId_init() public {
params.drawTimeout = 40; // there are 40 draws within the timeframe: 1-40
prizePool = newPrizePool();
assertEq(prizePool.getDrawIdPriorToShutdown(), 40, "draw id is the draw that ends before/on the timeout");
assertEq(prizePool.getShutdownDrawId(), 41, "draw id is the draw that ends before/on the timeout");
}

function test_getDrawIdPriorToShutdown_shift() public {
function test_getShutdownDrawId_shift() public {
params.drawTimeout = 40;
prizePool = newPrizePool();
awardDraw(winningRandomNumber);
assertEq(prizePool.getDrawIdPriorToShutdown(), 41, "draw id is the draw that ends before/on the timeout");
assertEq(prizePool.getShutdownDrawId(), 42, "draw id is the draw that ends before/on the timeout");
}

function test_getDrawIdPriorToShutdown_twabShutdownAtLimit() public {
params.drawPeriodSeconds = type(uint24).max - (type(uint24).max % 1 days);
params.drawTimeout = type(uint16).max;
params.grandPrizePeriodDraws = params.drawTimeout;
function test_getShutdownDrawId_twabShutdownAtLimit() public {
params.drawPeriodSeconds = 1 days;
params.drawTimeout = type(uint8).max;
params.grandPrizePeriodDraws = 365;
params.firstDrawOpensAt = uint48(twabController.lastObservationAt() - 1 days);
prizePool = newPrizePool();
awardDraw(winningRandomNumber);
assertEq(prizePool.getDrawIdPriorToShutdown(), 256, "draw id is the draw that ends before the twab controller shutdown");
assertEq(prizePool.getShutdownDrawId(), 2, "draw id is the draw that ends before the twab controller shutdown");
}

function testDrawTimeoutAt_init() public {
Expand Down Expand Up @@ -1309,6 +1309,9 @@ contract PrizePoolTest is Test {
function testWasClaimed_not() public {
assertEq(prizePool.wasClaimed(vault, msg.sender, 0, 0), false);
assertEq(prizePool.wasClaimed(vault2, msg.sender, 0, 0), false);

assertEq(prizePool.wasClaimed(vault, msg.sender, prizePool.getLastAwardedDrawId(), 0, 0), false);
assertEq(prizePool.wasClaimed(vault2, msg.sender, prizePool.getLastAwardedDrawId(), 0, 0), false);
}

function testWasClaimed_single() public {
Expand All @@ -1323,6 +1326,7 @@ contract PrizePoolTest is Test {
claimPrize(msg.sender, 1, 0);

assertEq(prizePool.wasClaimed(vault, msg.sender, 1, 0), true);
assertEq(prizePool.wasClaimed(vault, msg.sender, prizePool.getLastAwardedDrawId(), 1, 0), true);
}

function testWasClaimed_single_twoVaults() public {
Expand All @@ -1344,6 +1348,9 @@ contract PrizePoolTest is Test {

assertEq(prizePool.wasClaimed(vault, msg.sender, 1, 0), true);
assertEq(prizePool.wasClaimed(vault2, msg.sender, 1, 0), true);

assertEq(prizePool.wasClaimed(vault, msg.sender, prizePool.getLastAwardedDrawId(), 1, 0), true);
assertEq(prizePool.wasClaimed(vault2, msg.sender, prizePool.getLastAwardedDrawId(), 1, 0), true);
}

function testWasClaimed_old_draw() public {
Expand All @@ -1353,9 +1360,13 @@ contract PrizePoolTest is Test {
claimPrize(msg.sender, 0, 0);
assertEq(prizePool.wasClaimed(vault, msg.sender, 0, 0), true);
assertEq(prizePool.wasClaimed(vault2, msg.sender, 0, 0), false);
assertEq(prizePool.wasClaimed(vault, msg.sender, prizePool.getLastAwardedDrawId(), 0, 0), true);
assertEq(prizePool.wasClaimed(vault2, msg.sender, prizePool.getLastAwardedDrawId(), 0, 0), false);
awardDraw(winningRandomNumber);
assertEq(prizePool.wasClaimed(vault, msg.sender, 0, 0), false);
assertEq(prizePool.wasClaimed(vault2, msg.sender, 0, 0), false);
assertEq(prizePool.wasClaimed(vault, msg.sender, prizePool.getLastAwardedDrawId(), 0, 0), false);
assertEq(prizePool.wasClaimed(vault2, msg.sender, prizePool.getLastAwardedDrawId(), 0, 0), false);
}

function testAccountedBalance_remainder() public {
Expand Down Expand Up @@ -1866,6 +1877,48 @@ contract PrizePoolTest is Test {
prizePool.computeRangeStartDrawIdInclusive(1, 0);
}

function testGetTotalAccumulatorNewestObservation() public {
Observation memory initialObs = prizePool.getTotalAccumulatorNewestObservation();
assertEq(initialObs.available, 0);
assertEq(initialObs.disbursed, 0);

contribute(100e18);
Observation memory afterContributionObs = prizePool.getTotalAccumulatorNewestObservation();
assertEq(afterContributionObs.available, 100e18);
assertEq(afterContributionObs.disbursed, 0);

awardDraw(winningRandomNumber);
Observation memory afterAwardObs = prizePool.getTotalAccumulatorNewestObservation();
assertEq(afterContributionObs.available, 100e18);
assertEq(afterContributionObs.disbursed, 0);

contribute(100e18);
Observation memory after2ndContributionObs = prizePool.getTotalAccumulatorNewestObservation();
assertEq(after2ndContributionObs.available, 100e18);
assertEq(after2ndContributionObs.disbursed, 100e18); // new obs, so old available is moved to new disbursed
}

function testGetVaultAccumulatorNewestObservation() public {
Observation memory initialObs = prizePool.getVaultAccumulatorNewestObservation(address(this));
assertEq(initialObs.available, 0);
assertEq(initialObs.disbursed, 0);

contribute(100e18);
Observation memory afterContributionObs = prizePool.getVaultAccumulatorNewestObservation(address(this));
assertEq(afterContributionObs.available, 100e18);
assertEq(afterContributionObs.disbursed, 0);

awardDraw(winningRandomNumber);
Observation memory afterAwardObs = prizePool.getVaultAccumulatorNewestObservation(address(this));
assertEq(afterContributionObs.available, 100e18);
assertEq(afterContributionObs.disbursed, 0);

contribute(100e18);
Observation memory after2ndContributionObs = prizePool.getVaultAccumulatorNewestObservation(address(this));
assertEq(after2ndContributionObs.available, 100e18);
assertEq(after2ndContributionObs.disbursed, 100e18); // new obs, so old available is moved to new disbursed
}

// function mockGetAverageBalanceBetween(
// address _vault,
// address _user,
Expand Down Expand Up @@ -1989,9 +2042,9 @@ contract PrizePoolTest is Test {
}

function shutdownRangeDrawIds() public view returns (uint24, uint24) {
uint24 shutdownDrawId = prizePool.getDrawIdPriorToShutdown();
uint24 rangeStart = grandPrizeRangeStart(shutdownDrawId);
return (rangeStart, shutdownDrawId);
uint24 drawIdPriorToShutdown = prizePool.getShutdownDrawId() - 1;
uint24 rangeStart = grandPrizeRangeStart(drawIdPriorToShutdown);
return (rangeStart, drawIdPriorToShutdown);
}

function mockShutdownTwab(uint256 userTwab, uint256 totalSupplyTwab) public {
Expand Down

0 comments on commit f26119f

Please sign in to comment.