Skip to content

Commit

Permalink
Merge pull request #119 from GenerationSoftware/fix-review
Browse files Browse the repository at this point in the history
Sherlock fix review
  • Loading branch information
trmid authored Jul 16, 2024
2 parents a5810c1 + 34440a2 commit eb4adec
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 51 deletions.
1 change: 1 addition & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export MAINNET_RPC_URL=""
export ARBITRUM_RPC_URL=""
export OPTIMISM_RPC_URL=""
export POLYGON_RPC_URL=""
export BLAST_RPC_URL=""

# Testnet RPC URLs
export GOERLI_RPC_URL=""
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
- name: Run Forge test
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
BLAST_RPC_URL: ${{ secrets.BLAST_RPC_URL }}
run: |
forge test
id: test
Expand All @@ -42,6 +43,7 @@ jobs:
- name: Run Forge coverage
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
BLAST_RPC_URL: ${{ secrets.BLAST_RPC_URL }}
run: |
forge coverage --report lcov && lcov --remove lcov.info -o lcov.info 'test/*'
id: coverage
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mainnet = "${MAINNET_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"
optimism = "${OPTIMISM_RPC_URL}"
polygon = "${POLYGON_RPC_URL}"
blast = "${BLAST_RPC_URL}"

goerli = "${GOERLI_RPC_URL}"
arbitrum-goerli = "${ARBITRUM_GOERLI_RPC_URL}"
Expand Down
55 changes: 30 additions & 25 deletions src/PrizePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol";
import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import { SD59x18, convert, sd } from "prb-math/SD59x18.sol";
import { SD1x18, unwrap, UNIT } from "prb-math/SD1x18.sol";
import { UD60x18, convert } from "prb-math/UD60x18.sol";
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";

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

/* ============ Constants ============ */

// The minimum draw timeout. A timeout of two is necessary to allow for enough time to close and award a draw.
uint24 constant MINIMUM_DRAW_TIMEOUT = 2;

/* ============ Errors ============ */

/// @notice Thrown when the prize pool is constructed with a first draw open timestamp that is in the past
error FirstDrawOpensInPast();

Expand Down Expand Up @@ -89,8 +96,10 @@ error InvalidPrizeIndex(uint32 invalidPrizeIndex, uint32 prizeCount, uint8 tier)
/// @notice Thrown when there are no awarded draws when a computation requires an awarded draw.
error NoDrawsAwarded();

/// @notice Thrown when the Prize Pool is constructed with a draw timeout of zero
error DrawTimeoutIsZero();
/// @notice Thrown when the prize pool is initialized with a draw timeout lower than the minimum.
/// @param drawTimeout The draw timeout that was set
/// @param minimumDrawTimeout The minimum draw timeout
error DrawTimeoutLtMinimum(uint24 drawTimeout, uint24 minimumDrawTimeout);

/// @notice Thrown when the Prize Pool is constructed with a draw timeout greater than the grand prize period draws
error DrawTimeoutGTGrandPrizePeriodDraws();
Expand Down Expand Up @@ -158,14 +167,6 @@ struct ConstructorParams {
uint24 drawTimeout;
}

/// @notice A struct to represent a shutdown portion of liquidity for a vault and account
/// @param numerator The numerator of the portion
/// @param denominator The denominator of the portion
struct ShutdownPortion {
uint256 numerator;
uint256 denominator;
}

/// @title PoolTogether V5 Prize Pool
/// @author G9 Software Inc. & PoolTogether Inc. Team
/// @notice The Prize Pool holds the prize liquidity and allows vaults to claim prizes.
Expand Down Expand Up @@ -280,7 +281,7 @@ contract PrizePool is TieredLiquidityDistributor {
/// @notice The timestamp at which the first draw will open.
uint48 public immutable firstDrawOpensAt;

/// @notice The maximum number of draws that can be missed before the prize pool is considered inactive.
/// @notice The maximum number of draws that can pass since the last awarded draw before the prize pool is considered inactive.
uint24 public immutable drawTimeout;

/// @notice The address that is allowed to set the draw manager
Expand Down Expand Up @@ -317,7 +318,7 @@ contract PrizePool is TieredLiquidityDistributor {
mapping(address vault => mapping(address account => Observation lastWithdrawalTotalContributedObservation)) internal _withdrawalObservations;

/// @notice The shutdown portion of liquidity for a vault and account
mapping(address vault => mapping(address account => ShutdownPortion shutdownPortion)) internal _shutdownPortions;
mapping(address vault => mapping(address account => UD60x18 shutdownPortion)) internal _shutdownPortions;

/* ============ Constructor ============ */

Expand All @@ -335,8 +336,8 @@ contract PrizePool is TieredLiquidityDistributor {
params.grandPrizePeriodDraws
)
{
if (params.drawTimeout == 0) {
revert DrawTimeoutIsZero();
if (params.drawTimeout < MINIMUM_DRAW_TIMEOUT) {
revert DrawTimeoutLtMinimum(params.drawTimeout, MINIMUM_DRAW_TIMEOUT);
}

if (params.drawTimeout > params.grandPrizePeriodDraws) {
Expand Down Expand Up @@ -692,7 +693,7 @@ contract PrizePool is TieredLiquidityDistributor {
/// @return The number of draws
function getTierAccrualDurationInDraws(uint8 _tier) external view returns (uint24) {
return
uint24(TierCalculationLib.estimatePrizeFrequencyInDraws(getTierOdds(_tier, numberOfTiers)));
TierCalculationLib.estimatePrizeFrequencyInDraws(getTierOdds(_tier, numberOfTiers), grandPrizePeriodDraws);
}

/// @notice The total amount of prize tokens that have been withdrawn as fees or prizes
Expand Down Expand Up @@ -870,7 +871,7 @@ contract PrizePool is TieredLiquidityDistributor {
/// @param _vault The vault whose contributions are measured
/// @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) {
function computeShutdownPortion(address _vault, address _account) public view returns (UD60x18) {
uint24 drawIdPriorToShutdown = getShutdownDrawId() - 1;
uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(drawIdPriorToShutdown, grandPrizePeriodDraws);

Expand All @@ -887,11 +888,15 @@ contract PrizePool is TieredLiquidityDistributor {
drawIdPriorToShutdown
);

if (_vaultTwabTotalSupply == 0) {
return ShutdownPortion(0, 0);
if (_vaultTwabTotalSupply == 0 || totalContrib == 0) {
return UD60x18.wrap(0);
}

return ShutdownPortion(vaultContrib * _userTwab, totalContrib * _vaultTwabTotalSupply);
// first division purposely done before multiplication to avoid overflow
return convert(vaultContrib)
.div(convert(totalContrib))
.mul(convert(_userTwab))
.div(convert(_vaultTwabTotalSupply));
}

/// @notice Returns the shutdown balance for a given vault and account. The prize pool must already be shutdown.
Expand All @@ -907,7 +912,7 @@ contract PrizePool is TieredLiquidityDistributor {
}

Observation memory withdrawalObservation = _withdrawalObservations[_vault][_account];
ShutdownPortion memory shutdownPortion;
UD60x18 shutdownPortion;
uint256 balance;

// if we haven't withdrawn yet, add the portion of the shutdown balance
Expand All @@ -919,7 +924,7 @@ contract PrizePool is TieredLiquidityDistributor {
shutdownPortion = _shutdownPortions[_vault][_account];
}

if (shutdownPortion.denominator == 0) {
if (shutdownPortion.unwrap() == 0) {
return 0;
}

Expand All @@ -928,7 +933,7 @@ contract PrizePool is TieredLiquidityDistributor {
Observation memory newestObs = _totalAccumulator.newestObservation();
balance += (newestObs.available + newestObs.disbursed) - (withdrawalObservation.available + withdrawalObservation.disbursed);

return (shutdownPortion.numerator * balance) / shutdownPortion.denominator;
return convert(convert(balance).mul(shutdownPortion));
}

/// @notice Withdraws the shutdown balance for a given vault and sender
Expand Down Expand Up @@ -957,7 +962,7 @@ contract PrizePool is TieredLiquidityDistributor {
/// @notice Returns the timestamp at which the prize pool will be considered inactive and shutdown
/// @return The timestamp at which the prize pool will be considered inactive
function shutdownAt() public view returns (uint256) {
uint256 twabShutdownAt = twabController.lastObservationAt();
uint256 twabShutdownAt = drawOpensAt(getDrawId(twabController.lastObservationAt()));
uint256 drawTimeoutAt_ = drawTimeoutAt();
return drawTimeoutAt_ < twabShutdownAt ? drawTimeoutAt_ : twabShutdownAt;
}
Expand Down Expand Up @@ -1011,7 +1016,7 @@ contract PrizePool is TieredLiquidityDistributor {
}

SD59x18 tierOdds = getTierOdds(_tier, numberOfTiers);
uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(lastAwardedDrawId_, uint24(TierCalculationLib.estimatePrizeFrequencyInDraws(tierOdds)));
uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(lastAwardedDrawId_, TierCalculationLib.estimatePrizeFrequencyInDraws(tierOdds, grandPrizePeriodDraws));

uint32 tierPrizeCount = uint32(TierCalculationLib.prizeCount(_tier));

Expand Down
70 changes: 70 additions & 0 deletions src/extensions/BlastPrizePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { PrizePool, ConstructorParams } from "../PrizePool.sol";

// The rebasing WETH token on Blast
IERC20Rebasing constant WETH = IERC20Rebasing(0x4300000000000000000000000000000000000004);

/// @notice The Blast yield modes for WETH
enum YieldMode {
AUTOMATIC,
VOID,
CLAIMABLE
}

/// @notice The relevant interface for rebasing WETH on Blast
interface IERC20Rebasing {
function configure(YieldMode) external returns (uint256);
function claim(address recipient, uint256 amount) external returns (uint256);
function getClaimableAmount(address account) external view returns (uint256);
}

/// @notice Thrown if the prize token is not the expected token on Blast.
/// @param prizeToken The prize token address
/// @param expectedToken The expected token address
error PrizeTokenNotExpectedToken(address prizeToken, address expectedToken);

/// @notice Thrown if a yield donation is triggered when there is no claimable balance.
error NoClaimableBalance();

/// @title PoolTogether V5 Blast Prize Pool
/// @author G9 Software Inc.
/// @notice A modified prize pool that opts in to claimable WETH yield on Blast and allows anyone to trigger
/// a donation of the accrued yield to the prize pool.
contract BlastPrizePool is PrizePool {

/* ============ Constructor ============ */

/// @notice Constructs a new Blast Prize Pool.
/// @dev Reverts if the prize token is not the expected WETH token on Blast.
/// @param params A struct of constructor parameters
constructor(ConstructorParams memory params) PrizePool(params) {
if (address(params.prizeToken) != address(WETH)) {
revert PrizeTokenNotExpectedToken(address(params.prizeToken), address(WETH));
}

// Opt-in to claimable yield
WETH.configure(YieldMode.CLAIMABLE);
}

/* ============ External Functions ============ */

/// @notice Returns the claimable WETH yield balance for this contract
function claimableYieldBalance() external view returns (uint256) {
return WETH.getClaimableAmount(address(this));
}

/// @notice Claims the available WETH yield balance and donates it to the prize pool.
/// @return The amount claimed and donated.
function donateClaimableYield() external returns (uint256) {
uint256 _claimableYieldBalance = WETH.getClaimableAmount(address(this));
if (_claimableYieldBalance == 0) {
revert NoClaimableBalance();
}
WETH.claim(address(this), _claimableYieldBalance);
contributePrizeTokens(DONATOR, _claimableYieldBalance);
return _claimableYieldBalance;
}

}
7 changes: 5 additions & 2 deletions src/libraries/TierCalculationLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ library TierCalculationLib {
}

/// @notice Estimates the number of draws between a tier occurring.
/// @dev Limits the frequency to the grand prize period in draws.
/// @param _tierOdds The odds for the tier to calculate the frequency of
/// @param _grandPrizePeriod The number of draws between grand prizes
/// @return The estimated number of draws between the tier occurring
function estimatePrizeFrequencyInDraws(SD59x18 _tierOdds) internal pure returns (uint256) {
return uint256(convert(sd(1e18).div(_tierOdds).ceil()));
function estimatePrizeFrequencyInDraws(SD59x18 _tierOdds, uint24 _grandPrizePeriod) internal pure returns (uint24) {
uint256 _prizeFrequencyInDraws = uint256(convert(sd(1e18).div(_tierOdds).ceil()));
return _prizeFrequencyInDraws > _grandPrizePeriod ? _grandPrizePeriod : uint24(_prizeFrequencyInDraws);
}

/// @notice Computes the number of prizes for a given tier.
Expand Down
Loading

0 comments on commit eb4adec

Please sign in to comment.