├── .husky
└── pre-commit
├── .lintstagedrc
├── funding.json
├── .prettierignore
├── remappings.txt
├── .prettierrc
├── .solhint.json
├── .gitignore
├── test
├── mocks
│ └── ERC20Mintable.sol
├── invariants
│ ├── helpers
│ │ ├── CurrentTime.sol
│ │ ├── CurrentTimeConsumer.sol
│ │ ├── DrawAccumulatorFuzzHarness.sol
│ │ ├── TierCalculationFuzzHarness.sol
│ │ ├── TieredLiquidityDistributorFuzzHarness.sol
│ │ └── PrizePoolFuzzHarness.sol
│ ├── DrawAccumulatorInvariants.t.sol
│ ├── PrizePoolInvariants.t.sol
│ └── TieredLiquidityDistributorInvariants.t.sol
├── wrappers
│ ├── TierCalculationLibWrapper.sol
│ └── DrawAccumulatorLibWrapper.sol
├── abstract
│ ├── helper
│ │ └── TieredLiquidityDistributorWrapper.sol
│ └── TieredLiquidityDistributor.t.sol
├── extensions
│ └── BlastPrizePool.t.sol
└── libraries
│ ├── TierCalculationLib.t.sol
│ └── DrawAccumulatorLib.t.sol
├── .envrc.example
├── .gitmodules
├── LICENSE
├── package.json
├── .github
└── workflows
│ └── coverage.yml
├── foundry.toml
├── src
├── extensions
│ └── BlastPrizePool.sol
├── libraries
│ ├── TierCalculationLib.sol
│ └── DrawAccumulatorLib.sol
├── abstract
│ └── TieredLiquidityDistributor.sol
└── PrizePool.sol
└── README.md
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint-staged && npm test
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{json,md,sol,yml}": [
3 | "npm run format"
4 | ],
5 | "*.sol": [
6 | "npm run hint"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/funding.json:
--------------------------------------------------------------------------------
1 | {
2 | "opRetro": {
3 | "projectId": "0x45064f78302aeee0d4ea3667a39d3cffa25157b131aba8b1c0d9cfb7c085f87d"
4 | }
5 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | artifacts
2 | broadcast
3 | cache
4 | cache_hardhat
5 | deployments
6 | lib
7 | out
8 | types
9 |
10 | package.json
11 |
--------------------------------------------------------------------------------
/remappings.txt:
--------------------------------------------------------------------------------
1 | forge-std/=lib/forge-std/src/
2 | prb-math/=lib/prb-math/src/
3 | openzeppelin=lib/openzeppelin-contracts/contracts/
4 | pt-v5-twab-controller/=lib/pt-v5-twab-controller/src/
5 | ring-buffer-lib/=lib/ring-buffer-lib/src/
6 | uniform-random-number/=lib/uniform-random-number/src/
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "prettier-plugin-solidity"
4 | ],
5 | "overrides": [
6 | {
7 | "files": "*.sol",
8 | "options": {
9 | "bracketSpacing": true,
10 | "printWidth": 100,
11 | "tabWidth": 2
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "avoid-low-level-calls": "off",
6 | "compiler-version": ["error", "0.8.19"],
7 | "func-visibility": "off",
8 | "no-empty-blocks": "off",
9 | "no-inline-assembly": "off"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | coverage/
4 | out/
5 |
6 | # Ignores development broadcast logs
7 | !/broadcast
8 | /broadcast/*/31337/
9 | /broadcast/**/dry-run/
10 |
11 | # Docs
12 | docs/
13 |
14 | # Dotenv file
15 | .env
16 | .envrc
17 |
18 | # VSCode
19 | .history
20 |
21 | # Coverage
22 | lcov.info
23 |
24 | # Test Output Data
25 | data/
26 |
27 | .DS_Store
28 | node_modules
--------------------------------------------------------------------------------
/test/mocks/ERC20Mintable.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol";
5 |
6 | contract ERC20Mintable is ERC20 {
7 | constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {}
8 |
9 | function mint(address _account, uint256 _amount) public {
10 | _mint(_account, _amount);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/invariants/helpers/CurrentTime.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | /// @dev Because Foundry does not commit the state changes between invariant runs, we need to
5 | /// save the current timestamp in a contract with persistent storage.
6 | contract CurrentTime {
7 | uint256 public timestamp;
8 |
9 | constructor(uint256 _timestamp) {
10 | timestamp = _timestamp;
11 | }
12 |
13 | function set(uint _timestamp) external {
14 | timestamp = _timestamp;
15 | }
16 |
17 | function increase(uint256 _amount) external {
18 | timestamp += _amount;
19 | }
20 | }
--------------------------------------------------------------------------------
/test/invariants/DrawAccumulatorInvariants.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { DrawAccumulatorFuzzHarness } from "./helpers/DrawAccumulatorFuzzHarness.sol";
7 | import { Observation } from "../../src/libraries/DrawAccumulatorLib.sol";
8 |
9 | contract DrawAccumulatorInvariants is Test {
10 | DrawAccumulatorFuzzHarness public accumulator;
11 |
12 | function setUp() external {
13 | accumulator = new DrawAccumulatorFuzzHarness();
14 | }
15 |
16 | function invariant_future_plus_past_equals_total() external {
17 | Observation memory obs = accumulator.newestObservation();
18 | assertEq(obs.available + obs.disbursed, accumulator.totalAdded());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.envrc.example:
--------------------------------------------------------------------------------
1 | # Mnemonic phrase
2 | export MNEMONIC=""
3 |
4 | # Private key
5 | export PRIVATE_KEY=""
6 |
7 | # Mainnet RPC URLs
8 | export MAINNET_RPC_URL=""
9 | export ARBITRUM_RPC_URL=""
10 | export OPTIMISM_RPC_URL=""
11 | export POLYGON_RPC_URL=""
12 | export BLAST_RPC_URL=""
13 |
14 | # Testnet RPC URLs
15 | export GOERLI_RPC_URL=""
16 | export ARBITRUM_GOERLI_RPC_URL=""
17 | export OPTIMISM_GOERLI_RPC_URL=""
18 | export POLYGON_MUMBAI_RPC_URL=""
19 |
20 | # Used for verifying contracts on Etherscan
21 | export ETHERSCAN_API_KEY=""
22 | export ARBITRUM_ETHERSCAN_API_KEY=""
23 | export OPTIMISM_ETHERSCAN_API_KEY=""
24 | export POLYGONSCAN_API_KEY=""
25 |
26 | # Used to run Hardhat scripts in fork mode
27 | export FORK_ENABLED=true
28 |
29 | # Used to report gas usage when running Forge tests
30 | export FORGE_GAS_REPORT=true
31 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | branch = v1
5 | [submodule "lib/openzeppelin-contracts"]
6 | path = lib/openzeppelin-contracts
7 | url = https://github.com/openzeppelin/openzeppelin-contracts
8 | branch = release-v4.9
9 | [submodule "lib/prb-math"]
10 | path = lib/prb-math
11 | url = https://github.com/PaulRBerg/prb-math
12 | branch = v4
13 | [submodule "lib/ring-buffer-lib"]
14 | path = lib/ring-buffer-lib
15 | url = https://github.com/GenerationSoftware/ring-buffer-lib
16 | [submodule "lib/uniform-random-number"]
17 | path = lib/uniform-random-number
18 | url = https://github.com/generationsoftware/uniform-random-number
19 | [submodule "lib/pt-v5-twab-controller"]
20 | path = lib/pt-v5-twab-controller
21 | url = https://github.com/generationsoftware/pt-v5-twab-controller
22 |
--------------------------------------------------------------------------------
/test/invariants/helpers/CurrentTimeConsumer.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import { CommonBase } from "forge-std/Base.sol";
5 | import { CurrentTime } from "./CurrentTime.sol";
6 |
7 | contract CurrentTimeConsumer is CommonBase {
8 |
9 | CurrentTime public currentTime;
10 |
11 | modifier useCurrentTime() {
12 | warpCurrentTime();
13 | _;
14 | }
15 |
16 | modifier increaseCurrentTime(uint _amount) {
17 | currentTime.increase(_amount);
18 | warpCurrentTime();
19 | _;
20 | }
21 |
22 | function warpTo(uint _timestamp) internal {
23 | require(_timestamp > currentTime.timestamp(), "CurrentTimeConsumer/warpTo: cannot warp to the past");
24 | currentTime.set(_timestamp);
25 | warpCurrentTime();
26 | }
27 |
28 | function warpCurrentTime() internal {
29 | vm.warp(currentTime.timestamp());
30 | }
31 | }
--------------------------------------------------------------------------------
/test/wrappers/TierCalculationLibWrapper.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import { TierCalculationLib } from "../../src/libraries/TierCalculationLib.sol";
5 | import { SD59x18 } from "prb-math/SD59x18.sol";
6 | import { UD60x18 } from "prb-math/UD60x18.sol";
7 |
8 | // Note: Need to store the results from the library in a variable to be picked up by forge coverage
9 | // See: https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086
10 | contract TierCalculationLibWrapper {
11 | function tierPrizeCountPerDraw(uint8 _tier, SD59x18 _odds) external pure returns (uint32) {
12 | uint32 result = TierCalculationLib.tierPrizeCountPerDraw(_tier, _odds);
13 | return result;
14 | }
15 |
16 | function getTierOdds(
17 | uint8 _tier,
18 | uint8 _numberOfTiers,
19 | uint24 _grandPrizePeriod
20 | ) external pure returns (SD59x18) {
21 | SD59x18 result = TierCalculationLib.getTierOdds(_tier, _numberOfTiers, _grandPrizePeriod);
22 | return result;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/invariants/PrizePoolInvariants.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/console2.sol";
5 |
6 | import { PrizePoolFuzzHarness } from "./helpers/PrizePoolFuzzHarness.sol";
7 | import { Test } from "forge-std/Test.sol";
8 | import { CurrentTime, CurrentTimeConsumer } from "./helpers/CurrentTimeConsumer.sol";
9 |
10 | contract PrizePoolInvariants is Test, CurrentTimeConsumer {
11 | PrizePoolFuzzHarness public prizePoolHarness;
12 |
13 | function setUp() external {
14 | currentTime = new CurrentTime(365 days);
15 | prizePoolHarness = new PrizePoolFuzzHarness(currentTime);
16 | targetContract(address(prizePoolHarness));
17 | }
18 |
19 | function invariant_balance_equals_accounted() external useCurrentTime {
20 | uint balance = prizePoolHarness.token().balanceOf(address(prizePoolHarness.prizePool()));
21 | uint accounted = prizePoolHarness.prizePool().accountedBalance();
22 | assertEq(balance, accounted, "balance does not match accountedBalance");
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-2024 G9 Software Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@generationsoftware/pt-v5-prize-pool",
3 | "version": "1.0.0",
4 | "description": "PoolTogether V5 Prize Pool Contracts",
5 | "author": {
6 | "name": "G9 Software Inc.",
7 | "url": "https://github.com/GenerationSoftware"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/GenerationSoftware/pt-v5-prize-pool.git"
12 | },
13 | "scripts": {
14 | "clean": "forge clean",
15 | "compile": "forge compile",
16 | "coverage": "forge coverage --report lcov && lcov --extract lcov.info -o lcov.info 'src/*' && genhtml lcov.info -o coverage",
17 | "format": "prettier --config .prettierrc --write \"**/*.{json,md,sol,yml}\"",
18 | "format:file": "prettier --config .prettierrc --write",
19 | "hint": "solhint --config \"./.solhint.json\" \"src/**/*.sol\"",
20 | "lint-staged": "lint-staged",
21 | "prepack": "npm run clean && npm run compile",
22 | "prepare": "husky install",
23 | "test": "forge test"
24 | },
25 | "devDependencies": {
26 | "husky": "8.0.3",
27 | "lint-staged": "14.0.1",
28 | "prettier": "3.0.3",
29 | "prettier-plugin-solidity": "1.1.3",
30 | "solhint": "3.6.2",
31 | "solhint-plugin-prettier": "0.0.5"
32 | },
33 | "files": [
34 | "src/**",
35 | "out/**"
36 | ]
37 | }
--------------------------------------------------------------------------------
/test/invariants/helpers/DrawAccumulatorFuzzHarness.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/console2.sol";
5 |
6 | import { DrawAccumulatorLib, Observation } from "../../../src/libraries/DrawAccumulatorLib.sol";
7 | import { E, SD59x18, sd, unwrap } from "prb-math/SD59x18.sol";
8 |
9 | contract DrawAccumulatorFuzzHarness {
10 | using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator;
11 |
12 | uint256 public totalAdded;
13 |
14 | DrawAccumulatorLib.Accumulator internal accumulator;
15 |
16 | uint16 currentDrawId = uint8(uint256(blockhash(block.number - 1))) + 1;
17 |
18 | function add(uint88 _amount, uint8 _drawInc) public returns (bool) {
19 | currentDrawId += (_drawInc / 16);
20 | bool result = accumulator.add(_amount, currentDrawId);
21 | totalAdded += _amount;
22 | return result;
23 | }
24 |
25 | function getDisbursedBetween(uint16 _start, uint16 _end) external view returns (uint256 result) {
26 | uint24 start = _start % (currentDrawId*2);
27 | uint24 end = start + _end % (currentDrawId*2);
28 | result = accumulator.getDisbursedBetween(start, end);
29 | }
30 |
31 | function newestObservation() external view returns (Observation memory) {
32 | return accumulator.observations[accumulator.newestDrawId()];
33 | }
34 |
35 | function newestDrawId() external view returns (uint256) {
36 | return accumulator.newestDrawId();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: Tests with 100% Coverage
2 |
3 | on: ["push", "pull_request"]
4 |
5 | env:
6 | FOUNDRY_PROFILE: ci
7 |
8 | jobs:
9 | forge:
10 | strategy:
11 | fail-fast: true
12 | permissions:
13 | pull-requests: write
14 | name: Foundry project
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | submodules: recursive
20 |
21 | - name: Install Foundry
22 | uses: foundry-rs/foundry-toolchain@v1
23 | with:
24 | version: nightly
25 |
26 | - name: Run Forge build
27 | run: |
28 | forge --version
29 | forge build --sizes
30 | id: build
31 |
32 | - name: Run Forge test
33 | env:
34 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
35 | BLAST_RPC_URL: ${{ secrets.BLAST_RPC_URL }}
36 | run: |
37 | forge test
38 | id: test
39 |
40 | - name: Install lcov
41 | uses: hrishikesh-kadam/setup-lcov@v1.0.0
42 |
43 | - name: Run Forge coverage
44 | env:
45 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
46 | BLAST_RPC_URL: ${{ secrets.BLAST_RPC_URL }}
47 | run: |
48 | forge coverage --report lcov && lcov --remove lcov.info -o lcov.info 'test/*'
49 | id: coverage
50 |
51 | - name: Report code coverage
52 | uses: zgosalvez/github-actions-report-lcov@v1.5.0
53 | with:
54 | coverage-files: lcov.info
55 | minimum-coverage: 99
56 | github-token: ${{ secrets.GITHUB_TOKEN }}
57 |
--------------------------------------------------------------------------------
/test/invariants/helpers/TierCalculationFuzzHarness.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/console2.sol";
5 |
6 | import { TierCalculationLib } from "../../../src/libraries/TierCalculationLib.sol";
7 | import { SD59x18, unwrap, convert } from "prb-math/SD59x18.sol";
8 | import { CommonBase } from "forge-std/Base.sol";
9 |
10 | contract TierCalculationFuzzHarness is CommonBase {
11 | uint8 public immutable grandPrizePeriod = 10;
12 | uint128 immutable eachUserBalance = 100e18;
13 | SD59x18 immutable vaultPortion = convert(1);
14 | uint8 immutable _userCount = 20;
15 | uint8 public immutable numberOfTiers = 5;
16 |
17 | uint public winnerCount;
18 | uint32 public draws;
19 |
20 | function awardDraw(uint256 winningRandomNumber) public returns (uint) {
21 | uint drawPrizeCount;
22 | for (uint8 t = 0; t < numberOfTiers; t++) {
23 | uint32 prizeCount = uint32(TierCalculationLib.prizeCount(t));
24 | SD59x18 tierOdds = TierCalculationLib.getTierOdds(t, numberOfTiers, grandPrizePeriod);
25 | for (uint u = 1; u < _userCount + 1; u++) {
26 | address userAddress = vm.addr(u);
27 | for (uint32 p = 0; p < prizeCount; p++) {
28 | uint256 prn = TierCalculationLib.calculatePseudoRandomNumber(
29 | 1,
30 | address(this),
31 | userAddress,
32 | t,
33 | p,
34 | winningRandomNumber
35 | );
36 | if (
37 | TierCalculationLib.isWinner(
38 | prn,
39 | eachUserBalance,
40 | _userCount * eachUserBalance,
41 | vaultPortion,
42 | tierOdds
43 | )
44 | ) {
45 | drawPrizeCount++;
46 | }
47 | }
48 | }
49 | }
50 | winnerCount += drawPrizeCount;
51 | draws++;
52 | return drawPrizeCount;
53 | }
54 |
55 | function averagePrizesPerDraw() public view returns (uint256) {
56 | return winnerCount / draws;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = 'src'
3 | out = 'out'
4 | libs = ['lib']
5 | solc = "0.8.24"
6 | gas_reports_ignore = ["ERC20Mintable", "TwabController"]
7 | gas_limit = "18446744073709551615" # u64::MAX
8 | block_gas_limit = "18446744073709551615" # u64::MAX
9 | fs_permissions = [{ access = "read-write", path = "./data"}]
10 | optimizer = true
11 | via_ir = false
12 | ffi = true
13 |
14 | [profile.default.optimizer_details]
15 | peephole = true
16 | inliner = true
17 | jumpdest_remover = true
18 | order_literals = true
19 | deduplicate = true
20 | cse = true
21 | constant_optimizer = true
22 | yul = true
23 |
24 | [invariant]
25 | runs = 4
26 | depth = 400
27 |
28 | [fuzz]
29 | seed = "0x0ca1b799da18587180cdb11fc96564bf73af56a3f4e6981971452fc78cb0dcbc"
30 |
31 | [rpc_endpoints]
32 | mainnet = "${MAINNET_RPC_URL}"
33 | arbitrum = "${ARBITRUM_RPC_URL}"
34 | optimism = "${OPTIMISM_RPC_URL}"
35 | polygon = "${POLYGON_RPC_URL}"
36 | blast = "${BLAST_RPC_URL}"
37 |
38 | goerli = "${GOERLI_RPC_URL}"
39 | arbitrum-goerli = "${ARBITRUM_GOERLI_RPC_URL}"
40 | optimism-goerli = "${OPTIMISM_GOERLI_RPC_URL}"
41 | polygon-mumbai = "${POLYGON_MUMBAI_RPC_URL}"
42 |
43 | [etherscan]
44 | mainnet = { key = "${ETHERSCAN_API_KEY}", url = "https://api.etherscan.io/api" }
45 | arbitrum = { key = "${ARBITRUM_ETHERSCAN_API_KEY}", url = "https://api.arbiscan.io/api" }
46 | optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}", url = "https://api-optimistic.etherscan.io/api" }
47 | polygon = { key = "${POLYGONSCAN_API_KEY}", url = "https://api.polygonscan.com/api" }
48 |
49 | goerli = { key = "${ETHERSCAN_API_KEY}", url = "https://api-goerli.etherscan.io/api" }
50 | arbitrum-goerli = { key = "${ARBITRUM_ETHERSCAN_API_KEY}", url = "https://api-goerli.arbiscan.io/api" }
51 | optimism-goerli = { key = "${OPTIMISM_ETHERSCAN_API_KEY}", url = "https://api-goerli-optimistic.etherscan.io/api" }
52 | polygon-mumbai = { key = "${POLYGONSCAN_API_KEY}", url = "https://api-testnet.polygonscan.com/api" }
53 |
54 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config
55 |
--------------------------------------------------------------------------------
/test/abstract/helper/TieredLiquidityDistributorWrapper.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/console2.sol";
5 |
6 | import { TieredLiquidityDistributor, Tier } from "../../../src/abstract/TieredLiquidityDistributor.sol";
7 |
8 | contract TieredLiquidityDistributorWrapper is TieredLiquidityDistributor {
9 | constructor(
10 | uint256 _tierLiquidityUtilizationRate,
11 | uint8 _numberOfTiers,
12 | uint8 _tierShares,
13 | uint8 _canaryShares,
14 | uint8 _reserveShares,
15 | uint24 _grandPrizePeriodDraws
16 | )
17 | TieredLiquidityDistributor(_tierLiquidityUtilizationRate, _numberOfTiers, _tierShares, _canaryShares, _reserveShares, _grandPrizePeriodDraws)
18 | {}
19 |
20 | function awardDraw(uint8 _nextNumTiers, uint256 liquidity) external {
21 | _awardDraw(_lastAwardedDrawId + 1, _nextNumTiers, liquidity);
22 | }
23 |
24 | function consumeLiquidity(uint8 _tier, uint96 _liquidity) external {
25 | Tier memory _tierData = _getTier(_tier, numberOfTiers);
26 | _consumeLiquidity(_tierData, _tier, _liquidity);
27 | }
28 |
29 | function computeNewDistributions(
30 | uint8 _numberOfTiers,
31 | uint8 _nextNumberOfTiers,
32 | uint128 _currentPrizeTokenPerShare,
33 | uint256 _prizeTokenLiquidity
34 | ) external view returns (uint96, uint128) {
35 | (uint96 newReserve, uint128 newPrizeTokenPerShare) = _computeNewDistributions(
36 | _numberOfTiers,
37 | _nextNumberOfTiers,
38 | _currentPrizeTokenPerShare,
39 | _prizeTokenLiquidity
40 | );
41 | return (newReserve, newPrizeTokenPerShare);
42 | }
43 |
44 | function estimateNumberOfTiersUsingPrizeCountPerDraw(
45 | uint32 _prizeCount
46 | ) external view returns (uint8) {
47 | uint8 result = _estimateNumberOfTiersUsingPrizeCountPerDraw(_prizeCount);
48 | return result;
49 | }
50 |
51 | function sumTierPrizeCounts(uint8 _numTiers) external view returns (uint32) {
52 | uint32 result = _sumTierPrizeCounts(_numTiers);
53 | return result;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/invariants/helpers/TieredLiquidityDistributorFuzzHarness.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import {
5 | TieredLiquidityDistributor,
6 | Tier,
7 | convert,
8 | MINIMUM_NUMBER_OF_TIERS
9 | } from "../../../src/abstract/TieredLiquidityDistributor.sol";
10 |
11 | contract TieredLiquidityDistributorFuzzHarness is TieredLiquidityDistributor {
12 | uint256 public totalAdded;
13 | uint256 public totalConsumed;
14 |
15 | constructor() TieredLiquidityDistributor(1e18, MINIMUM_NUMBER_OF_TIERS, 100, 5, 10, 365) {}
16 |
17 | function awardDraw(uint8 _nextNumTiers, uint96 liquidity) external {
18 | uint8 nextNumTiers = _nextNumTiers / 16; // map to [0, 15]
19 | nextNumTiers = nextNumTiers < MINIMUM_NUMBER_OF_TIERS ? MINIMUM_NUMBER_OF_TIERS : nextNumTiers; // ensure min tiers
20 | totalAdded += liquidity;
21 | _awardDraw(_lastAwardedDrawId + 1, nextNumTiers, liquidity);
22 | }
23 |
24 | function net() external view returns (uint256) {
25 | return totalAdded - totalConsumed;
26 | }
27 |
28 | function accountedLiquidity() external view returns (uint256) {
29 | uint256 availableLiquidity;
30 |
31 | for (uint8 i = 0; i < numberOfTiers; i++) {
32 | Tier memory tier = _getTier(i, numberOfTiers);
33 | availableLiquidity += _getTierRemainingLiquidity(
34 | tier.prizeTokenPerShare,
35 | prizeTokenPerShare,
36 | _numShares(i, numberOfTiers)
37 | );
38 | }
39 |
40 | availableLiquidity += _reserve;
41 |
42 | return availableLiquidity;
43 | }
44 |
45 | function consumeLiquidity(uint8 _tier) external {
46 | uint8 tier = _tier % numberOfTiers;
47 |
48 | Tier memory tier_ = _getTier(tier, numberOfTiers);
49 | uint104 liq = uint104(
50 | _getTierRemainingLiquidity(
51 | tier_.prizeTokenPerShare,
52 | prizeTokenPerShare,
53 | _numShares(_tier, numberOfTiers)
54 | )
55 | );
56 |
57 | // half the time consume only half
58 | if (_tier > 128) {
59 | liq += _reserve / 2;
60 | }
61 |
62 | totalConsumed += liq;
63 | _consumeLiquidity(tier_, tier, liq);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/test/invariants/TieredLiquidityDistributorInvariants.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { TieredLiquidityDistributorFuzzHarness } from "./helpers/TieredLiquidityDistributorFuzzHarness.sol";
7 |
8 | contract TieredLiquidityDistributorInvariants is Test {
9 | TieredLiquidityDistributorFuzzHarness public distributor;
10 |
11 | function setUp() external {
12 | distributor = new TieredLiquidityDistributorFuzzHarness();
13 | }
14 |
15 | function testTiers_always_sum() external {
16 | uint256 expected = distributor.totalAdded() - distributor.totalConsumed();
17 | uint256 accounted = distributor.accountedLiquidity();
18 |
19 | // Uncomment to append delta data to local CSV file:
20 | // --------------------------------------------------------
21 | // uint256 delta = expected > accounted ? expected - accounted : accounted - expected;
22 | // vm.writeLine(string.concat(vm.projectRoot(), "/data/tiers_accounted_liquidity_delta.csv"), string.concat(vm.toString(distributor.numberOfTiers()), ",", vm.toString(delta)));
23 | // assertApproxEqAbs(accounted, expected, 50); // run with high ceiling to avoid failures while recording data
24 | // --------------------------------------------------------
25 | // Comment out to avoid failing test while recording data:
26 | assertEq(accounted, expected, "accounted equals expected");
27 | // --------------------------------------------------------
28 | }
29 |
30 | // Failure case regression test (2023-05-26)
31 | function testInvariantFailure_Case_2023_05_26() external {
32 | distributor.awardDraw(4, 253012247290373118207);
33 | distributor.awardDraw(4, 99152290762372054017);
34 | distributor.awardDraw(255, 792281625142643375935439);
35 | distributor.consumeLiquidity(1);
36 | distributor.consumeLiquidity(0);
37 | distributor.awardDraw(1, 2365);
38 | distributor.awardDraw(5, 36387);
39 | distributor.awardDraw(74, 486356342973499764);
40 | distributor.consumeLiquidity(174);
41 | distributor.consumeLiquidity(254);
42 | distributor.awardDraw(6, 2335051495798885129312);
43 | distributor.awardDraw(160, 543634559793817062402);
44 | distributor.awardDraw(187, 3765046993999626249);
45 | distributor.awardDraw(1, 196958881398058173458);
46 | uint256 expected = distributor.totalAdded() - distributor.totalConsumed();
47 | assertEq(distributor.accountedLiquidity(), expected);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/extensions/BlastPrizePool.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.24;
3 |
4 | import { PrizePool, ConstructorParams } from "../PrizePool.sol";
5 |
6 | // The rebasing WETH token on Blast
7 | IERC20Rebasing constant WETH = IERC20Rebasing(0x4300000000000000000000000000000000000004);
8 |
9 | /// @notice The Blast yield modes for WETH
10 | enum YieldMode {
11 | AUTOMATIC,
12 | VOID,
13 | CLAIMABLE
14 | }
15 |
16 | /// @notice The relevant interface for rebasing WETH on Blast
17 | interface IERC20Rebasing {
18 | function configure(YieldMode) external returns (uint256);
19 | function claim(address recipient, uint256 amount) external returns (uint256);
20 | function getClaimableAmount(address account) external view returns (uint256);
21 | }
22 |
23 | /// @notice Thrown if the prize token is not the expected token on Blast.
24 | /// @param prizeToken The prize token address
25 | /// @param expectedToken The expected token address
26 | error PrizeTokenNotExpectedToken(address prizeToken, address expectedToken);
27 |
28 | /// @notice Thrown if a yield donation is triggered when there is no claimable balance.
29 | error NoClaimableBalance();
30 |
31 | /// @title PoolTogether V5 Blast Prize Pool
32 | /// @author G9 Software Inc.
33 | /// @notice A modified prize pool that opts in to claimable WETH yield on Blast and allows anyone to trigger
34 | /// a donation of the accrued yield to the prize pool.
35 | contract BlastPrizePool is PrizePool {
36 |
37 | /* ============ Constructor ============ */
38 |
39 | /// @notice Constructs a new Blast Prize Pool.
40 | /// @dev Reverts if the prize token is not the expected WETH token on Blast.
41 | /// @param params A struct of constructor parameters
42 | constructor(ConstructorParams memory params) PrizePool(params) {
43 | if (address(params.prizeToken) != address(WETH)) {
44 | revert PrizeTokenNotExpectedToken(address(params.prizeToken), address(WETH));
45 | }
46 |
47 | // Opt-in to claimable yield
48 | WETH.configure(YieldMode.CLAIMABLE);
49 | }
50 |
51 | /* ============ External Functions ============ */
52 |
53 | /// @notice Returns the claimable WETH yield balance for this contract
54 | function claimableYieldBalance() external view returns (uint256) {
55 | return WETH.getClaimableAmount(address(this));
56 | }
57 |
58 | /// @notice Claims the available WETH yield balance and donates it to the prize pool.
59 | /// @return The amount claimed and donated.
60 | function donateClaimableYield() external returns (uint256) {
61 | uint256 _claimableYieldBalance = WETH.getClaimableAmount(address(this));
62 | if (_claimableYieldBalance == 0) {
63 | revert NoClaimableBalance();
64 | }
65 | WETH.claim(address(this), _claimableYieldBalance);
66 | contributePrizeTokens(DONATOR, _claimableYieldBalance);
67 | return _claimableYieldBalance;
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/test/wrappers/DrawAccumulatorLibWrapper.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.19;
4 |
5 | import { DrawAccumulatorLib, Observation, RingBufferInfo } from "../../src/libraries/DrawAccumulatorLib.sol";
6 | import { RingBufferLib } from "ring-buffer-lib/RingBufferLib.sol";
7 | import { E, SD59x18, sd, unwrap } from "prb-math/SD59x18.sol";
8 |
9 | // Note: Need to store the results from the library in a variable to be picked up by forge coverage
10 | // See: https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086
11 | contract DrawAccumulatorLibWrapper {
12 | DrawAccumulatorLib.Accumulator internal accumulator;
13 |
14 | function getRingBufferInfo() public view returns (RingBufferInfo memory) {
15 | return accumulator.ringBufferInfo;
16 | }
17 |
18 | function getDrawRingBuffer(uint16 index) public view returns (uint24) {
19 | return accumulator.drawRingBuffer[index];
20 | }
21 |
22 | function setDrawRingBuffer(uint16 index, uint8 value) public {
23 | accumulator.drawRingBuffer[index] = value;
24 | }
25 |
26 | function getCardinality() public view returns (uint16) {
27 | return accumulator.ringBufferInfo.cardinality;
28 | }
29 |
30 | function getNextIndex() public view returns (uint16) {
31 | return accumulator.ringBufferInfo.nextIndex;
32 | }
33 |
34 | function setRingBufferInfo(uint16 nextIndex, uint16 cardinality) public {
35 | accumulator.ringBufferInfo.cardinality = cardinality;
36 | accumulator.ringBufferInfo.nextIndex = nextIndex;
37 | }
38 |
39 | function getObservation(uint24 drawId) public view returns (Observation memory) {
40 | return accumulator.observations[drawId];
41 | }
42 |
43 | function add(uint256 _amount, uint24 _drawId) public returns (bool) {
44 | bool result = DrawAccumulatorLib.add(accumulator, _amount, _drawId);
45 | return result;
46 | }
47 |
48 | function newestObservation() public view returns (Observation memory) {
49 | Observation memory result = DrawAccumulatorLib.newestObservation(accumulator);
50 | return result;
51 | }
52 |
53 | /**
54 | * Requires endDrawId to be greater than (the newest draw id - 1)
55 | */
56 | function getDisbursedBetween(
57 | uint24 _startDrawId,
58 | uint24 _endDrawId
59 | ) public view returns (uint256) {
60 | uint256 result = DrawAccumulatorLib.getDisbursedBetween(
61 | accumulator,
62 | _startDrawId,
63 | _endDrawId
64 | );
65 | return result;
66 | }
67 |
68 | /**
69 | */
70 | function binarySearch(
71 | uint16 _oldestIndex,
72 | uint16 _newestIndex,
73 | uint16 _cardinality,
74 | uint24 _targetLastClosedDrawId
75 | )
76 | public
77 | view
78 | returns (
79 | uint16 beforeOrAtIndex,
80 | uint24 beforeOrAtDrawId,
81 | uint16 afterOrAtIndex,
82 | uint24 afterOrAtDrawId
83 | )
84 | {
85 | (beforeOrAtIndex, beforeOrAtDrawId, afterOrAtIndex, afterOrAtDrawId) = DrawAccumulatorLib
86 | .binarySearch(
87 | accumulator.drawRingBuffer,
88 | _oldestIndex,
89 | _newestIndex,
90 | _cardinality,
91 | _targetLastClosedDrawId
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/test/extensions/BlastPrizePool.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.24;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
7 | import { BlastPrizePool, ConstructorParams, WETH, PrizeTokenNotExpectedToken, NoClaimableBalance } from "../../src/extensions/BlastPrizePool.sol";
8 | import { IERC20 } from "../../src/PrizePool.sol";
9 |
10 | contract BlastPrizePoolTest is Test {
11 | BlastPrizePool prizePool;
12 |
13 | address bob = makeAddr("bob");
14 | address alice = makeAddr("alice");
15 |
16 | address wethWhale = address(0x66714DB8F3397c767d0A602458B5b4E3C0FE7dd1);
17 |
18 | TwabController twabController;
19 | IERC20 prizeToken;
20 | address drawManager;
21 |
22 | uint256 TIER_SHARES = 100;
23 | uint256 CANARY_SHARES = 5;
24 | uint256 RESERVE_SHARES = 10;
25 |
26 | uint24 grandPrizePeriodDraws = 365;
27 | uint48 drawPeriodSeconds = 1 days;
28 | uint24 drawTimeout;
29 | uint48 firstDrawOpensAt;
30 | uint8 initialNumberOfTiers = 4;
31 | uint256 winningRandomNumber = 123456;
32 | uint256 tierLiquidityUtilizationRate = 1e18;
33 |
34 | uint256 blockNumber = 5213491;
35 | uint256 blockTimestamp = 1719236797;
36 |
37 | ConstructorParams params;
38 |
39 | function setUp() public {
40 | drawTimeout = 30;
41 |
42 | vm.createSelectFork("blast", blockNumber);
43 | vm.warp(blockTimestamp);
44 |
45 | prizeToken = IERC20(address(WETH));
46 | twabController = new TwabController(uint32(drawPeriodSeconds), uint32(blockTimestamp - 1 days));
47 |
48 | firstDrawOpensAt = uint48(blockTimestamp + 1 days); // set draw start 1 day into future
49 |
50 | drawManager = address(this);
51 |
52 | params = ConstructorParams(
53 | prizeToken,
54 | twabController,
55 | drawManager,
56 | tierLiquidityUtilizationRate,
57 | drawPeriodSeconds,
58 | firstDrawOpensAt,
59 | grandPrizePeriodDraws,
60 | initialNumberOfTiers,
61 | uint8(TIER_SHARES),
62 | uint8(CANARY_SHARES),
63 | uint8(RESERVE_SHARES),
64 | drawTimeout
65 | );
66 |
67 | prizePool = new BlastPrizePool(params);
68 | prizePool.setDrawManager(address(this));
69 | }
70 |
71 | function testWrongPrizeToken() public {
72 | params.prizeToken = IERC20(address(1));
73 | vm.expectRevert(abi.encodeWithSelector(PrizeTokenNotExpectedToken.selector, address(1), address(WETH)));
74 | prizePool = new BlastPrizePool(params);
75 | }
76 |
77 | function testClaimableYield() public {
78 | assertEq(IERC20(address(WETH)).balanceOf(address(prizePool)), 0);
79 |
80 | // check balance
81 | assertEq(prizePool.claimableYieldBalance(), 0);
82 |
83 | // donate some tokens to the prize pool
84 | vm.startPrank(wethWhale);
85 | IERC20(address(WETH)).approve(address(prizePool), 1e18);
86 | prizePool.donatePrizeTokens(1e18);
87 | vm.stopPrank();
88 | assertEq(prizePool.getDonatedBetween(1, 1), 1e18);
89 |
90 | // deal some ETH to the WETH contract and call addValue
91 | deal(address(WETH), 1e18 + address(WETH).balance);
92 | vm.startPrank(address(0x4300000000000000000000000000000000000000)); // REPORTER
93 | (bool success,) = address(WETH).call(abi.encodeWithSignature("addValue(uint256)", 0));
94 | vm.stopPrank();
95 | require(success, "addValue failed");
96 |
97 | // check balance non-zero
98 | uint256 claimable = prizePool.claimableYieldBalance();
99 | assertGt(claimable, 0);
100 |
101 | // trigger donation
102 | vm.startPrank(alice);
103 | uint256 donated = prizePool.donateClaimableYield();
104 | vm.stopPrank();
105 |
106 | assertEq(donated, claimable);
107 | assertEq(prizePool.getDonatedBetween(1, 1), 1e18 + donated);
108 | assertEq(prizePool.claimableYieldBalance(), 0);
109 |
110 | // reverts on donation of zero balance
111 | vm.expectRevert(abi.encodeWithSelector(NoClaimableBalance.selector));
112 | prizePool.donateClaimableYield();
113 | }
114 |
115 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # PoolTogether V5 Prize Pool
8 |
9 | [](https://github.com/generationsoftware/pt-v5-prize-pool/actions/workflows/coverage.yml)
10 | [](https://docs.openzeppelin.com/)
11 | 
12 |
13 | Have questions or want the latest news?
14 |
Join the PoolTogether Discord or follow us on Twitter:
15 |
16 | [](https://pooltogether.com/discord)
17 | [](https://twitter.com/PoolTogether_)
18 |
19 | ## Overview
20 |
21 | In PoolTogether V5 prizes are distributed through the Prize Pool contract. There is one Prize Pool deployed on each chain on which PT is deployed. The Prize Pool receives POOL tokens from Vaults, and releases the tokens as prizes in daily Draws. In this way, prize liquidity is isolated to a chain.
22 |
23 | - Accrued yield is sold by the Liquidator and sent to the Prize Pool.
24 | - Every "Draw" a random number is provided and given to the Prize Pool and the next set of prizes are available.
25 | - The Prize Pool determines a users chance of winning by reading historic data from the TWAB Controller.
26 |
27 | > [!important]
28 | > Only WETH has been audited as the `prizeToken` for the prize pool. Other prize tokens may lead to unknown complications due to differences in precision or mathematical limitations.
29 |
30 | ## Development
31 |
32 | ### Installation
33 |
34 | You may have to install the following tools to use this repository:
35 |
36 | - [Foundry](https://github.com/foundry-rs/foundry) to compile and test contracts
37 | - [direnv](https://direnv.net/) to handle environment variables
38 | - [lcov](https://github.com/linux-test-project/lcov) to generate the code coverage report
39 |
40 | Install dependencies:
41 |
42 | ```
43 | npm i
44 | ```
45 |
46 | ### Env
47 |
48 | Copy `.envrc.example` and write down the env variables needed to run this project.
49 |
50 | ```
51 | cp .envrc.example .envrc
52 | ```
53 |
54 | Once your env variables are setup, load them with:
55 |
56 | ```
57 | direnv allow
58 | ```
59 |
60 | ### Compile
61 |
62 | Run the following command to compile the contracts:
63 |
64 | ```
65 | npm run compile
66 | ```
67 |
68 | ### Coverage
69 |
70 | Forge is used for coverage, run it with:
71 |
72 | ```
73 | npm run coverage
74 | ```
75 |
76 | You can then consult the report by opening `coverage/index.html`:
77 |
78 | ```
79 | open coverage/index.html
80 | ```
81 |
82 | ### Code quality
83 |
84 | [Husky](https://typicode.github.io/husky/#/) is used to run [lint-staged](https://github.com/okonet/lint-staged) and tests when committing.
85 |
86 | [Prettier](https://prettier.io) is used to format TypeScript and Solidity code. Use it by running:
87 |
88 | ```
89 | npm run format
90 | ```
91 |
92 | [Solhint](https://protofire.github.io/solhint/) is used to lint Solidity files. Run it with:
93 |
94 | ```
95 | npm run hint
96 | ```
97 |
98 | ### Tests
99 |
100 | Test names including `SLOW` will be skipped on default test runs and need to be explicitly run.
101 |
102 | ### CI
103 |
104 | A default Github Actions workflow is setup to execute on push and pull request.
105 |
106 | It will build the contracts and run the test coverage.
107 |
108 | You can modify it here: [.github/workflows/coverage.yml](.github/workflows/coverage.yml)
109 |
110 | For the coverage to work, you will need to setup the `MAINNET_RPC_URL` repository secret in the settings of your Github repository.
111 |
--------------------------------------------------------------------------------
/test/libraries/TierCalculationLib.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { TierCalculationLib } from "../../src/libraries/TierCalculationLib.sol";
7 | import { MINIMUM_NUMBER_OF_TIERS, MAXIMUM_NUMBER_OF_TIERS } from "../../src/abstract/TieredLiquidityDistributor.sol";
8 | import { TierCalculationLibWrapper } from "../wrappers/TierCalculationLibWrapper.sol";
9 | import { SD59x18, sd, wrap, unwrap, convert } from "prb-math/SD59x18.sol";
10 | import { UD60x18, ud } from "prb-math/UD60x18.sol";
11 |
12 | contract TierCalculationLibTest is Test {
13 | TierCalculationLibWrapper wrapper;
14 |
15 | function setUp() public {
16 | wrapper = new TierCalculationLibWrapper();
17 | }
18 |
19 | function testGetTierOdds_grandPrizeOdds() public {
20 | for (uint8 i = MINIMUM_NUMBER_OF_TIERS - 1; i <= MAXIMUM_NUMBER_OF_TIERS; i++) {
21 | // grand prize is always 1/365
22 | assertEq(unwrap(wrapper.getTierOdds(0, i, 365)), 2739726027397260);
23 | }
24 | }
25 |
26 | function testGetTierOdds_tier4() public {
27 | assertEq(unwrap(wrapper.getTierOdds(0, 4, 365)), 2739726027397260);
28 | assertEq(unwrap(wrapper.getTierOdds(1, 4, 365)), 8089033552608040);
29 | assertEq(unwrap(wrapper.getTierOdds(2, 4, 365)), 33163436331078433);
30 | assertEq(unwrap(wrapper.getTierOdds(3, 4, 365)), 1e18);
31 | }
32 |
33 | function testEstimatePrizeFrequencyInDraws() public {
34 | assertEq(
35 | TierCalculationLib.estimatePrizeFrequencyInDraws(TierCalculationLib.getTierOdds(0, 4, 365), 365),
36 | 365
37 | );
38 | assertEq(
39 | TierCalculationLib.estimatePrizeFrequencyInDraws(TierCalculationLib.getTierOdds(1, 4, 365), 365),
40 | 124
41 | );
42 | assertEq(
43 | TierCalculationLib.estimatePrizeFrequencyInDraws(TierCalculationLib.getTierOdds(2, 4, 365), 365),
44 | 31
45 | );
46 | assertEq(
47 | TierCalculationLib.estimatePrizeFrequencyInDraws(TierCalculationLib.getTierOdds(3, 4, 365), 365),
48 | 1
49 | );
50 | }
51 |
52 | function testPrizeCount() public {
53 | assertEq(TierCalculationLib.prizeCount(0), 1);
54 | }
55 |
56 | function testCalculateWinningZoneWithTierOdds() public {
57 | assertEq(TierCalculationLib.calculateWinningZone(1000, sd(0.333e18), sd(1e18)), 333);
58 | }
59 |
60 | function testCalculateWinningZoneWithVaultPortion() public {
61 | assertEq(TierCalculationLib.calculateWinningZone(1000, sd(1e18), sd(0.444e18)), 444);
62 | }
63 |
64 | function testCalculateWinningZoneWithPrizeCount() public {
65 | assertEq(TierCalculationLib.calculateWinningZone(1000, sd(1e18), sd(1e18)), 1000);
66 | }
67 |
68 | function testIsWinner_WinsAll() external {
69 | uint8 tier = 5;
70 | uint8 numberOfTiers = 6;
71 | vm.assume(tier < numberOfTiers);
72 | uint16 grandPrizePeriod = 365;
73 | SD59x18 tierOdds = TierCalculationLib.getTierOdds(tier, numberOfTiers, grandPrizePeriod);
74 | uint32 prizeCount = uint32(TierCalculationLib.prizeCount(tier));
75 | SD59x18 vaultContribution = convert(int256(1));
76 |
77 | uint wins;
78 | for (uint i = 0; i < prizeCount; i++) {
79 | if (
80 | TierCalculationLib.isWinner(
81 | uint256(keccak256(abi.encode(i))),
82 | 1000,
83 | 1000,
84 | vaultContribution,
85 | tierOdds
86 | )
87 | ) {
88 | wins++;
89 | }
90 | }
91 |
92 | assertApproxEqAbs(wins, prizeCount, 0);
93 | }
94 |
95 | function testIsWinner_HalfLiquidity() external {
96 | uint8 tier = 5;
97 | uint8 numberOfTiers = 6;
98 | vm.assume(tier < numberOfTiers);
99 | uint16 grandPrizePeriod = 365;
100 | SD59x18 tierOdds = TierCalculationLib.getTierOdds(tier, numberOfTiers, grandPrizePeriod);
101 | uint32 prizeCount = uint32(TierCalculationLib.prizeCount(tier));
102 | SD59x18 vaultContribution = convert(int256(1));
103 |
104 | uint wins;
105 | for (uint i = 0; i < prizeCount; i++) {
106 | if (
107 | TierCalculationLib.isWinner(
108 | uint256(keccak256(abi.encode(i))),
109 | 500,
110 | 1000,
111 | vaultContribution,
112 | tierOdds
113 | )
114 | ) {
115 | wins++;
116 | }
117 | }
118 |
119 | assertApproxEqAbs(wins, prizeCount / 2, 20);
120 | }
121 |
122 | function testTierPrizeCountPerDraw() public {
123 | assertEq(wrapper.tierPrizeCountPerDraw(3, wrap(0.5e18)), 32);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/libraries/TierCalculationLib.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.19;
4 |
5 | import { UniformRandomNumber } from "uniform-random-number/UniformRandomNumber.sol";
6 | import { SD59x18, sd, unwrap, convert } from "prb-math/SD59x18.sol";
7 |
8 | /// @title Tier Calculation Library
9 | /// @author PoolTogether Inc. Team
10 | /// @notice Provides helper functions to assist in calculating tier prize counts, frequency, and odds.
11 | library TierCalculationLib {
12 | /// @notice Calculates the odds of a tier occurring.
13 | /// @param _tier The tier to calculate odds for
14 | /// @param _numberOfTiers The total number of tiers
15 | /// @param _grandPrizePeriod The number of draws between grand prizes
16 | /// @return The odds that a tier should occur for a single draw.
17 | function getTierOdds(
18 | uint8 _tier,
19 | uint8 _numberOfTiers,
20 | uint24 _grandPrizePeriod
21 | ) internal pure returns (SD59x18) {
22 | int8 oneMinusNumTiers = 1 - int8(_numberOfTiers);
23 | return
24 | sd(1).div(sd(int24(_grandPrizePeriod))).pow(
25 | sd(int8(_tier) + oneMinusNumTiers).div(sd(oneMinusNumTiers)).sqrt()
26 | );
27 | }
28 |
29 | /// @notice Estimates the number of draws between a tier occurring.
30 | /// @dev Limits the frequency to the grand prize period in draws.
31 | /// @param _tierOdds The odds for the tier to calculate the frequency of
32 | /// @param _grandPrizePeriod The number of draws between grand prizes
33 | /// @return The estimated number of draws between the tier occurring
34 | function estimatePrizeFrequencyInDraws(SD59x18 _tierOdds, uint24 _grandPrizePeriod) internal pure returns (uint24) {
35 | uint256 _prizeFrequencyInDraws = uint256(convert(sd(1e18).div(_tierOdds).ceil()));
36 | return _prizeFrequencyInDraws > _grandPrizePeriod ? _grandPrizePeriod : uint24(_prizeFrequencyInDraws);
37 | }
38 |
39 | /// @notice Computes the number of prizes for a given tier.
40 | /// @param _tier The tier to compute for
41 | /// @return The number of prizes
42 | function prizeCount(uint8 _tier) internal pure returns (uint256) {
43 | return 4 ** _tier;
44 | }
45 |
46 | /// @notice Determines if a user won a prize tier.
47 | /// @param _userSpecificRandomNumber The random number to use as entropy
48 | /// @param _userTwab The user's time weighted average balance
49 | /// @param _vaultTwabTotalSupply The vault's time weighted average total supply
50 | /// @param _vaultContributionFraction The portion of the prize that was contributed by the vault
51 | /// @param _tierOdds The odds of the tier occurring
52 | /// @return True if the user won the tier, false otherwise
53 | function isWinner(
54 | uint256 _userSpecificRandomNumber,
55 | uint256 _userTwab,
56 | uint256 _vaultTwabTotalSupply,
57 | SD59x18 _vaultContributionFraction,
58 | SD59x18 _tierOdds
59 | ) internal pure returns (bool) {
60 | if (_vaultTwabTotalSupply == 0) {
61 | return false;
62 | }
63 |
64 | /// The user-held portion of the total supply is the "winning zone".
65 | /// If the above pseudo-random number falls within the winning zone, the user has won this tier.
66 | /// However, we scale the size of the zone based on:
67 | /// - Odds of the tier occurring
68 | /// - Number of prizes
69 | /// - Portion of prize that was contributed by the vault
70 |
71 | return
72 | UniformRandomNumber.uniform(_userSpecificRandomNumber, _vaultTwabTotalSupply) <
73 | calculateWinningZone(_userTwab, _vaultContributionFraction, _tierOdds);
74 | }
75 |
76 | /// @notice Calculates a pseudo-random number that is unique to the user, tier, and winning random number.
77 | /// @param _drawId The draw id the user is checking
78 | /// @param _vault The vault the user deposited into
79 | /// @param _user The user
80 | /// @param _tier The tier
81 | /// @param _prizeIndex The particular prize index they are checking
82 | /// @param _winningRandomNumber The winning random number
83 | /// @return A pseudo-random number
84 | function calculatePseudoRandomNumber(
85 | uint24 _drawId,
86 | address _vault,
87 | address _user,
88 | uint8 _tier,
89 | uint32 _prizeIndex,
90 | uint256 _winningRandomNumber
91 | ) internal pure returns (uint256) {
92 | return
93 | uint256(
94 | keccak256(abi.encode(_drawId, _vault, _user, _tier, _prizeIndex, _winningRandomNumber))
95 | );
96 | }
97 |
98 | /// @notice Calculates the winning zone for a user. If their pseudo-random number falls within this zone, they win the tier.
99 | /// @param _userTwab The user's time weighted average balance
100 | /// @param _vaultContributionFraction The portion of the prize that was contributed by the vault
101 | /// @param _tierOdds The odds of the tier occurring
102 | /// @return The winning zone for the user.
103 | function calculateWinningZone(
104 | uint256 _userTwab,
105 | SD59x18 _vaultContributionFraction,
106 | SD59x18 _tierOdds
107 | ) internal pure returns (uint256) {
108 | return
109 | uint256(convert(convert(int256(_userTwab)).mul(_tierOdds).mul(_vaultContributionFraction)));
110 | }
111 |
112 | /// @notice Computes the estimated number of prizes per draw for a given tier and tier odds.
113 | /// @param _tier The tier
114 | /// @param _odds The odds of the tier occurring for the draw
115 | /// @return The estimated number of prizes per draw for the given tier and tier odds
116 | function tierPrizeCountPerDraw(uint8 _tier, SD59x18 _odds) internal pure returns (uint32) {
117 | return uint32(uint256(unwrap(sd(int256(prizeCount(_tier))).mul(_odds))));
118 | }
119 |
120 | /// @notice Checks whether a tier is a valid tier
121 | /// @param _tier The tier to check
122 | /// @param _numberOfTiers The number of tiers
123 | /// @return True if the tier is valid, false otherwise
124 | function isValidTier(uint8 _tier, uint8 _numberOfTiers) internal pure returns (bool) {
125 | return _tier < _numberOfTiers;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/test/invariants/helpers/PrizePoolFuzzHarness.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/console2.sol";
5 |
6 | import { CommonBase } from "forge-std/Base.sol";
7 | import { StdCheats } from "forge-std/StdCheats.sol";
8 | import { StdUtils } from "forge-std/StdUtils.sol";
9 | import { UD2x18 } from "prb-math/UD2x18.sol";
10 | import { SD1x18 } from "prb-math/SD1x18.sol";
11 | import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
12 |
13 | import { CurrentTime, CurrentTimeConsumer } from "./CurrentTimeConsumer.sol";
14 | import { PrizePool, ConstructorParams } from "../../../src/PrizePool.sol";
15 | import { MINIMUM_NUMBER_OF_TIERS } from "../../../src/abstract/TieredLiquidityDistributor.sol";
16 | import { ERC20Mintable } from "../../mocks/ERC20Mintable.sol";
17 |
18 | contract PrizePoolFuzzHarness is CommonBase, StdCheats, StdUtils, CurrentTimeConsumer {
19 | PrizePool public prizePool;
20 | ERC20Mintable public token;
21 | TwabController twabController;
22 |
23 | uint256 public contributed;
24 | uint256 public withdrawn;
25 | uint256 public claimed;
26 |
27 | address claimer;
28 |
29 | address[4] public actors;
30 | address internal currentActor;
31 |
32 | uint256 tierLiquidityUtilizationRate = 1e18;
33 | address drawManager = address(this);
34 | uint48 drawPeriodSeconds = 1 hours;
35 | uint48 awardDrawStartsAt;
36 | uint24 grandPrizePeriod = 365;
37 | uint8 numberOfTiers = MINIMUM_NUMBER_OF_TIERS;
38 | uint8 tierShares = 100;
39 | uint8 canaryShares = 5;
40 | uint8 reserveShares = 10;
41 | uint24 drawTimeout = 5;
42 |
43 | constructor(CurrentTime _currentTime) {
44 | currentTime = _currentTime;
45 | warpCurrentTime();
46 | // console2.log("constructor 1");
47 | claimer = makeAddr("claimer");
48 | // console2.log("constructor 2");
49 | for (uint i = 0; i != actors.length; i++) {
50 | actors[i] = makeAddr(string(abi.encodePacked("actor", i)));
51 | }
52 | // console2.log("constructor 3");
53 |
54 | // console2.log("constructor 4");
55 |
56 | awardDrawStartsAt = uint48(currentTime.timestamp());
57 |
58 | // console2.log("constructor 4.1");
59 |
60 | token = new ERC20Mintable("name", "SYMBOL");
61 | twabController = new TwabController(
62 | uint32(drawPeriodSeconds),
63 | uint32(awardDrawStartsAt)
64 | );
65 | // console2.log("constructor 5");
66 | // arbitrary mint
67 | twabController.mint(address(this), 100e18);
68 |
69 | // console2.log("constructor 6");
70 | ConstructorParams memory params = ConstructorParams(
71 | token,
72 | twabController,
73 | drawManager,
74 | tierLiquidityUtilizationRate,
75 | drawPeriodSeconds,
76 | awardDrawStartsAt,
77 | grandPrizePeriod,
78 | numberOfTiers,
79 | tierShares,
80 | canaryShares,
81 | reserveShares,
82 | drawTimeout
83 | );
84 |
85 | // console2.log("constructor 7");
86 |
87 | prizePool = new PrizePool(params);
88 | }
89 |
90 | function deposit(uint88 _amount, uint256 actorSeed) public useCurrentTime prankActor(actorSeed) {
91 | twabController.mint(_actor(actorSeed), _amount);
92 | }
93 |
94 | function contributePrizeTokens(uint88 _amount, uint256 actorSeed) public increaseCurrentTime(_timeIncrease()) prankActor(actorSeed) {
95 | // console2.log("contributePrizeTokens");
96 | contributed += _amount;
97 | token.mint(address(prizePool), _amount);
98 | prizePool.contributePrizeTokens(_actor(actorSeed), _amount);
99 | }
100 |
101 | function donatePrizeTokens(uint88 _amount, uint256 actorSeed) public increaseCurrentTime(_timeIncrease()) prankActor(actorSeed) {
102 | // console2.log("contributePrizeTokens");
103 | address actor = _actor(actorSeed);
104 | token.mint(address(actor), _amount);
105 | vm.startPrank(actor);
106 | token.approve(address(prizePool), _amount);
107 | contributed += _amount;
108 | prizePool.donatePrizeTokens(_amount);
109 | vm.stopPrank();
110 | }
111 |
112 | function contributeReserve(uint88 _amount, uint256 actorSeed) public increaseCurrentTime(_timeIncrease()) prankActor(actorSeed) {
113 | if (prizePool.isShutdown()) {
114 | return;
115 | }
116 | contributed += _amount;
117 | token.mint(_actor(actorSeed), _amount);
118 | token.approve(address(prizePool), _amount);
119 | prizePool.contributeReserve(_amount);
120 | }
121 |
122 | function shutdown() public increaseCurrentTime(_timeIncrease()) {
123 | vm.warp(prizePool.shutdownAt());
124 | }
125 |
126 | function allocateRewardFromReserve(uint256 actorSeed) public increaseCurrentTime(_timeIncrease()) prankDrawManager {
127 | // console2.log("allocateRewardFromReserve");
128 | uint96 amount = prizePool.reserve();
129 | withdrawn += amount;
130 | prizePool.allocateRewardFromReserve(_actor(actorSeed), amount);
131 | }
132 |
133 | function withdrawClaimReward() public increaseCurrentTime(_timeIncrease()) {
134 | // console2.log("withdrawClaimReward");
135 | vm.startPrank(claimer);
136 | prizePool.withdrawRewards(address(claimer), prizePool.rewardBalance(claimer));
137 | vm.stopPrank();
138 | }
139 |
140 | function claimPrizes() public useCurrentTime {
141 | // console2.log("claimPrizes");
142 | // console2.log("prizePool.numberOfTiers()", prizePool.numberOfTiers());
143 | if (prizePool.getLastAwardedDrawId() == 0) {
144 | // console2.log("skiipping");
145 | return;
146 | }
147 | for (uint i = 0; i < actors.length; i++) {
148 | _claimFor(actors[i]);
149 | }
150 | }
151 |
152 | function withdrawShutdownBalance(uint256 _actorSeed) public increaseCurrentTime(_timeIncrease()) prankActor(_actorSeed) {
153 | // console2.log("withdrawShutdownBalance withdrawShutdownBalance withdrawShutdownBalance withdrawShutdownBalance");
154 | address actor = _actor(_actorSeed);
155 | if (prizePool.shutdownBalanceOf(address(this), actor) > 0) {
156 | // console2.log("HAS A SHUTDOWN BALANCE");
157 | }
158 | prizePool.withdrawShutdownBalance(address(this), actor);
159 | }
160 |
161 | function awardDraw() public useCurrentTime prankDrawManager {
162 | // console2.log("AWARDING");
163 | uint24 drawId = prizePool.getDrawIdToAward();
164 | uint256 drawToAwardClosesAt = prizePool.drawClosesAt(drawId);
165 | if (drawToAwardClosesAt > currentTime.timestamp()) {
166 | warpTo(drawToAwardClosesAt);
167 | }
168 | prizePool.awardDraw(uint256(keccak256(abi.encode(block.timestamp))));
169 | // console2.log("SUCCESSSSS AWARDED DRAW");
170 | }
171 |
172 | function _actor(uint256 actorIndexSeed) internal view returns (address) {
173 | return actors[_bound(actorIndexSeed, 0, actors.length - 1)];
174 | }
175 |
176 | function _timeIncrease() internal view returns (uint256) {
177 | uint amount = _bound(uint256(keccak256(abi.encode(block.timestamp))), 0, drawPeriodSeconds/2);
178 | // console2.log("amount", amount);
179 | return amount;
180 | }
181 |
182 | function _claimFor(address actor_) internal {
183 | uint8 numTiers = prizePool.numberOfTiers();
184 | for (uint8 i = 0; i < numTiers; i++) {
185 | for (uint32 p = 0; p < prizePool.getTierPrizeCount(i); p++) {
186 | if (
187 | prizePool.isWinner(address(this), actor_, i, p) &&
188 | !prizePool.wasClaimed(address(this), actor_, i, p) &&
189 | prizePool.claimCount() < 4**2 // prevent claiming all prizes
190 | ) {
191 | // console2.log("CLAIMING");
192 | uint256 prizeSize = prizePool.getTierPrizeSize(i);
193 | if (prizeSize > 0) {
194 | claimed += prizePool.claimPrize(
195 | actor_,
196 | i,
197 | p,
198 | address(this),
199 | uint96(prizeSize/10),
200 | address(claimer)
201 | );
202 | }
203 | }
204 | }
205 | }
206 | }
207 |
208 | modifier prankActor(uint256 actorIndexSeed) {
209 | currentActor = _actor(actorIndexSeed);
210 | vm.startPrank(currentActor);
211 | _;
212 | vm.stopPrank();
213 | }
214 |
215 | modifier prankDrawManager() {
216 | vm.startPrank(drawManager);
217 | _;
218 | vm.stopPrank();
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/libraries/DrawAccumulatorLib.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.24;
4 |
5 | import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
6 | import { RingBufferLib } from "ring-buffer-lib/RingBufferLib.sol";
7 |
8 | // The maximum number of observations that can be recorded.
9 | uint16 constant MAX_OBSERVATION_CARDINALITY = 366;
10 |
11 | /// @notice Thrown when adding balance for draw zero.
12 | error AddToDrawZero();
13 |
14 | /// @notice Thrown when an action can't be done on a closed draw.
15 | /// @param drawId The ID of the closed draw
16 | /// @param newestDrawId The newest draw ID
17 | error DrawAwarded(uint24 drawId, uint24 newestDrawId);
18 |
19 | /// @notice Thrown when a draw range is not strictly increasing.
20 | /// @param startDrawId The start draw ID of the range
21 | /// @param endDrawId The end draw ID of the range
22 | error InvalidDrawRange(uint24 startDrawId, uint24 endDrawId);
23 |
24 | /// @notice The accumulator observation record
25 | /// @param available The total amount available as of this Observation
26 | /// @param disbursed The total amount disbursed in the past
27 | struct Observation {
28 | uint96 available;
29 | uint160 disbursed;
30 | }
31 |
32 | /// @notice The metadata for using the ring buffer.
33 | struct RingBufferInfo {
34 | uint16 nextIndex;
35 | uint16 cardinality;
36 | }
37 |
38 | /// @title Draw Accumulator Lib
39 | /// @author G9 Software Inc.
40 | /// @notice This contract distributes tokens over time according to an exponential weighted average.
41 | /// Time is divided into discrete "draws", of which each is allocated tokens.
42 | library DrawAccumulatorLib {
43 |
44 | /// @notice An accumulator for a draw.
45 | /// @param ringBufferInfo The metadata for the drawRingBuffer
46 | /// @param drawRingBuffer The ring buffer of draw ids
47 | /// @param observations The observations for each draw id
48 | struct Accumulator {
49 | RingBufferInfo ringBufferInfo; // 32 bits
50 | uint24[366] drawRingBuffer; // 8784 bits
51 | // 8784 + 32 = 8816 bits in total
52 | // 256 * 35 = 8960
53 | // 8960 - 8816 = 144 bits left over
54 | mapping(uint256 drawId => Observation observation) observations;
55 | }
56 |
57 | /// @notice Adds balance for the given draw id to the accumulator.
58 | /// @param accumulator The accumulator to add to
59 | /// @param _amount The amount of balance to add
60 | /// @param _drawId The draw id to which to add balance to. This must be greater than or equal to the previous
61 | /// addition's draw id.
62 | /// @return True if a new observation was created, false otherwise.
63 | function add(
64 | Accumulator storage accumulator,
65 | uint256 _amount,
66 | uint24 _drawId
67 | ) internal returns (bool) {
68 | if (_drawId == 0) {
69 | revert AddToDrawZero();
70 | }
71 | RingBufferInfo memory ringBufferInfo = accumulator.ringBufferInfo;
72 |
73 | uint24 newestDrawId_ = accumulator.drawRingBuffer[
74 | RingBufferLib.newestIndex(ringBufferInfo.nextIndex, MAX_OBSERVATION_CARDINALITY)
75 | ];
76 |
77 | if (_drawId < newestDrawId_) {
78 | revert DrawAwarded(_drawId, newestDrawId_);
79 | }
80 |
81 | mapping(uint256 drawId => Observation observation) storage accumulatorObservations = accumulator
82 | .observations;
83 | Observation memory newestObservation_ = accumulatorObservations[newestDrawId_];
84 | if (_drawId != newestDrawId_) {
85 | uint16 cardinality = ringBufferInfo.cardinality;
86 | if (ringBufferInfo.cardinality < MAX_OBSERVATION_CARDINALITY) {
87 | cardinality += 1;
88 | } else {
89 | // Delete the old observation to save gas (older than 1 year)
90 | delete accumulatorObservations[accumulator.drawRingBuffer[ringBufferInfo.nextIndex]];
91 | }
92 |
93 | accumulator.drawRingBuffer[ringBufferInfo.nextIndex] = _drawId;
94 | accumulatorObservations[_drawId] = Observation({
95 | available: SafeCast.toUint96(_amount),
96 | disbursed: SafeCast.toUint160(
97 | newestObservation_.disbursed +
98 | newestObservation_.available
99 | )
100 | });
101 |
102 | accumulator.ringBufferInfo = RingBufferInfo({
103 | nextIndex: uint16(RingBufferLib.nextIndex(ringBufferInfo.nextIndex, MAX_OBSERVATION_CARDINALITY)),
104 | cardinality: cardinality
105 | });
106 |
107 | return true;
108 | } else {
109 | accumulatorObservations[newestDrawId_] = Observation({
110 | available: SafeCast.toUint96(newestObservation_.available + _amount),
111 | disbursed: newestObservation_.disbursed
112 | });
113 |
114 | return false;
115 | }
116 | }
117 |
118 | /// @notice Returns the newest draw id from the accumulator.
119 | /// @param accumulator The accumulator to get the newest draw id from
120 | /// @return The newest draw id
121 | function newestDrawId(Accumulator storage accumulator) internal view returns (uint256) {
122 | return
123 | accumulator.drawRingBuffer[
124 | RingBufferLib.newestIndex(accumulator.ringBufferInfo.nextIndex, MAX_OBSERVATION_CARDINALITY)
125 | ];
126 | }
127 |
128 | /// @notice Returns the newest draw id from the accumulator.
129 | /// @param accumulator The accumulator to get the newest draw id from
130 | /// @return The newest draw id
131 | function newestObservation(Accumulator storage accumulator) internal view returns (Observation memory) {
132 | return accumulator.observations[
133 | newestDrawId(accumulator)
134 | ];
135 | }
136 |
137 | /// @notice Gets the balance that was disbursed between the given start and end draw ids, inclusive.
138 | /// @param _accumulator The accumulator to get the disbursed balance from
139 | /// @param _startDrawId The start draw id, inclusive
140 | /// @param _endDrawId The end draw id, inclusive
141 | /// @return The disbursed balance between the given start and end draw ids, inclusive
142 | function getDisbursedBetween(
143 | Accumulator storage _accumulator,
144 | uint24 _startDrawId,
145 | uint24 _endDrawId
146 | ) internal view returns (uint256) {
147 | if (_startDrawId > _endDrawId) {
148 | revert InvalidDrawRange(_startDrawId, _endDrawId);
149 | }
150 |
151 | RingBufferInfo memory ringBufferInfo = _accumulator.ringBufferInfo;
152 |
153 | if (ringBufferInfo.cardinality == 0) {
154 | return 0;
155 | }
156 |
157 | uint16 oldestIndex = uint16(
158 | RingBufferLib.oldestIndex(
159 | ringBufferInfo.nextIndex,
160 | ringBufferInfo.cardinality,
161 | MAX_OBSERVATION_CARDINALITY
162 | )
163 | );
164 | uint16 newestIndex = uint16(
165 | RingBufferLib.newestIndex(ringBufferInfo.nextIndex, ringBufferInfo.cardinality)
166 | );
167 |
168 | uint24 oldestDrawId = _accumulator.drawRingBuffer[oldestIndex];
169 | uint24 _newestDrawId = _accumulator.drawRingBuffer[newestIndex];
170 |
171 | if (_endDrawId < oldestDrawId || _startDrawId > _newestDrawId) {
172 | // if out of range, return 0
173 | return 0;
174 | }
175 |
176 | Observation memory atOrAfterStart;
177 | if (_startDrawId <= oldestDrawId || ringBufferInfo.cardinality == 1) {
178 | atOrAfterStart = _accumulator.observations[oldestDrawId];
179 | } else {
180 | // check if the start draw has an observation, otherwise search for the earliest observation after
181 | atOrAfterStart = _accumulator.observations[_startDrawId];
182 | if (atOrAfterStart.available == 0 && atOrAfterStart.disbursed == 0) {
183 | (, , , uint24 afterOrAtDrawId) = binarySearch(
184 | _accumulator.drawRingBuffer,
185 | oldestIndex,
186 | newestIndex,
187 | ringBufferInfo.cardinality,
188 | _startDrawId
189 | );
190 | atOrAfterStart = _accumulator.observations[afterOrAtDrawId];
191 | }
192 | }
193 |
194 | Observation memory atOrBeforeEnd;
195 | if (_endDrawId >= _newestDrawId || ringBufferInfo.cardinality == 1) {
196 | atOrBeforeEnd = _accumulator.observations[_newestDrawId];
197 | } else {
198 | // check if the end draw has an observation, otherwise search for the latest observation before
199 | atOrBeforeEnd = _accumulator.observations[_endDrawId];
200 | if (atOrBeforeEnd.available == 0 && atOrBeforeEnd.disbursed == 0) {
201 | (, uint24 beforeOrAtDrawId, , ) = binarySearch(
202 | _accumulator.drawRingBuffer,
203 | oldestIndex,
204 | newestIndex,
205 | ringBufferInfo.cardinality,
206 | _endDrawId
207 | );
208 | atOrBeforeEnd = _accumulator.observations[beforeOrAtDrawId];
209 | }
210 | }
211 |
212 | return atOrBeforeEnd.available + atOrBeforeEnd.disbursed - atOrAfterStart.disbursed;
213 | }
214 |
215 | /// @notice Binary searches an array of draw ids for the given target draw id.
216 | /// @dev The _targetDrawId MUST exist between the range of draws at _oldestIndex and _newestIndex (inclusive)
217 | /// @param _drawRingBuffer The array of draw ids to search
218 | /// @param _oldestIndex The oldest index in the ring buffer
219 | /// @param _newestIndex The newest index in the ring buffer
220 | /// @param _cardinality The number of items in the ring buffer
221 | /// @param _targetDrawId The target draw id to search for
222 | /// @return beforeOrAtIndex The index of the observation occurring at or before the target draw id
223 | /// @return beforeOrAtDrawId The draw id of the observation occurring at or before the target draw id
224 | /// @return afterOrAtIndex The index of the observation occurring at or after the target draw id
225 | /// @return afterOrAtDrawId The draw id of the observation occurring at or after the target draw id
226 | function binarySearch(
227 | uint24[366] storage _drawRingBuffer,
228 | uint16 _oldestIndex,
229 | uint16 _newestIndex,
230 | uint16 _cardinality,
231 | uint24 _targetDrawId
232 | )
233 | internal
234 | view
235 | returns (
236 | uint16 beforeOrAtIndex,
237 | uint24 beforeOrAtDrawId,
238 | uint16 afterOrAtIndex,
239 | uint24 afterOrAtDrawId
240 | )
241 | {
242 | uint16 leftSide = _oldestIndex;
243 | uint16 rightSide = _newestIndex < leftSide ? leftSide + _cardinality - 1 : _newestIndex;
244 | uint16 currentIndex;
245 |
246 | while (true) {
247 | // We start our search in the middle of the `leftSide` and `rightSide`.
248 | // After each iteration, we narrow down the search to the left or the right side while still starting our search in the middle.
249 | currentIndex = (leftSide + rightSide) / 2;
250 |
251 | beforeOrAtIndex = uint16(RingBufferLib.wrap(currentIndex, _cardinality));
252 | beforeOrAtDrawId = _drawRingBuffer[beforeOrAtIndex];
253 |
254 | afterOrAtIndex = uint16(RingBufferLib.nextIndex(currentIndex, _cardinality));
255 | afterOrAtDrawId = _drawRingBuffer[afterOrAtIndex];
256 |
257 | bool targetAtOrAfter = beforeOrAtDrawId <= _targetDrawId;
258 |
259 | // Check if we've found the corresponding Observation.
260 | if (targetAtOrAfter && _targetDrawId <= afterOrAtDrawId) {
261 | break;
262 | }
263 |
264 | // If `beforeOrAtTimestamp` is greater than `_target`, then we keep searching lower. To the left of the current index.
265 | if (!targetAtOrAfter) {
266 | rightSide = currentIndex - 1;
267 | } else {
268 | // Otherwise, we keep searching higher. To the left of the current index.
269 | leftSide = currentIndex + 1;
270 | }
271 | }
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/test/libraries/DrawAccumulatorLib.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { SD59x18, sd } from "prb-math/SD59x18.sol";
7 |
8 | import { DrawAccumulatorLib, AddToDrawZero, DrawAwarded, InvalidDrawRange, Observation } from "../../src/libraries/DrawAccumulatorLib.sol";
9 | import { DrawAccumulatorLibWrapper } from "../wrappers/DrawAccumulatorLibWrapper.sol";
10 |
11 | contract DrawAccumulatorLibTest is Test {
12 | using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator;
13 |
14 | DrawAccumulatorLib.Accumulator accumulator;
15 | DrawAccumulatorLibWrapper wrapper;
16 | uint contribution = 10000;
17 |
18 | function setUp() public {
19 | /*
20 | Alpha of 0.9 and contribution of 10000 result in disbursal of:
21 |
22 | 1 1000
23 | 2 900
24 | 3 810
25 | 4 729
26 | 5 656
27 | 6 590
28 | ...
29 | */
30 | wrapper = new DrawAccumulatorLibWrapper();
31 | }
32 |
33 | function testAdd_emitsAddToDrawZero() public {
34 | vm.expectRevert(abi.encodeWithSelector(AddToDrawZero.selector));
35 | add(0);
36 | }
37 |
38 | function testAdd_emitsDrawAwarded() public {
39 | add(4);
40 | vm.expectRevert(abi.encodeWithSelector(DrawAwarded.selector, 3, 4));
41 | add(3);
42 | }
43 |
44 | function testAddOne() public {
45 | DrawAccumulatorLib.add(accumulator, 100, 1);
46 | assertEq(accumulator.ringBufferInfo.cardinality, 1);
47 | assertEq(accumulator.ringBufferInfo.nextIndex, 1);
48 | assertEq(accumulator.drawRingBuffer[0], 1);
49 | assertEq(accumulator.observations[1].available, 100);
50 | }
51 |
52 | function testAddSame() public {
53 | wrapper.add(100, 1);
54 | wrapper.add(200, 1);
55 |
56 | assertEq(wrapper.getCardinality(), 1);
57 | assertEq(wrapper.getRingBufferInfo().nextIndex, 1);
58 | assertEq(wrapper.getDrawRingBuffer(0), 1);
59 | assertEq(wrapper.getObservation(1).available, 300);
60 | }
61 |
62 | function testAddSecond() public {
63 | DrawAccumulatorLib.add(accumulator, 100, 1);
64 | DrawAccumulatorLib.add(accumulator, 200, 3);
65 |
66 | assertEq(accumulator.ringBufferInfo.cardinality, 2);
67 | assertEq(accumulator.ringBufferInfo.nextIndex, 2);
68 | assertEq(accumulator.drawRingBuffer[0], 1);
69 | assertEq(accumulator.drawRingBuffer[1], 3);
70 |
71 | // 100 - 19 = 81
72 |
73 | assertEq(accumulator.observations[3].available, 200);
74 | }
75 |
76 | function testAddOne_deleteExpired() public {
77 | // set up accumulator as if we had just completed a buffer loop:
78 | for (uint16 i = 0; i < 366; i++) {
79 | wrapper.add(100, i + 1);
80 | assertEq(wrapper.getCardinality(), i + 1);
81 | assertEq(wrapper.getNextIndex(), i == 365 ? 0 : i + 1);
82 | assertEq(wrapper.getDrawRingBuffer(i), i + 1);
83 | assertGe(wrapper.getObservation(i + 1).available, wrapper.getObservation(i).available);
84 | }
85 |
86 | assertEq(wrapper.getCardinality(), 366);
87 |
88 | wrapper.add(200, 367);
89 | assertEq(wrapper.getCardinality(), 366);
90 | assertEq(wrapper.getNextIndex(), 1);
91 | assertEq(wrapper.getDrawRingBuffer(0), 367);
92 | assertGt(wrapper.getObservation(367).available, wrapper.getObservation(366).available);
93 | assertEq(wrapper.getObservation(1).available, 0); // deleted draw 1
94 | }
95 |
96 | function test_newestObservation() public {
97 | wrapper.add(100, 1);
98 | Observation memory observation = wrapper.newestObservation();
99 | assertEq(observation.available, 100);
100 |
101 | wrapper.add(200, 2);
102 | observation = wrapper.newestObservation();
103 | assertEq(observation.available, 200);
104 | assertEq(observation.disbursed, 100);
105 | }
106 |
107 | function testGetDisbursedBetweenEmpty() public {
108 | assertEq(getDisbursedBetween(1, 4), 0);
109 | }
110 |
111 | function testGetDisbursedBetween_invalidRange() public {
112 | vm.expectRevert(abi.encodeWithSelector(InvalidDrawRange.selector, 2, 1));
113 | getDisbursedBetween(2, 1);
114 | }
115 |
116 | function testGetDisbursedBetween_endMoreThanOneBeforeLast() public {
117 | add(1);
118 | add(2);
119 | add(4);
120 | assertEq(getDisbursedBetween(1, 2), 2e5); // end draw ID is more than 2 before last observation (4)
121 | }
122 |
123 | function testGetDisbursedBetween_endOneLessThanLast() public {
124 | add(1); // 1000
125 | add(2); // 1000 + 900
126 | // 900 + 810
127 | add(4); // 1000 + 810 + 729
128 | assertApproxEqAbs(getDisbursedBetween(1, 3), 2e5, 1); // end draw ID is 1 before last observation (4)
129 | }
130 |
131 | function testGetDisbursedBetween_binarySearchBothStartAndEnd() public {
132 | // here we want to test a case where the algorithm must binary search both the start and end observations
133 | add(1); // 1000
134 | add(2); // 1000 + 900
135 | add(3); // 1000 + 900 + 810
136 | add(4); // 1000 + 900 + 810 + 729
137 | add(5);
138 | add(6);
139 | assertApproxEqAbs(getDisbursedBetween(3, 4), 2e5, 1); // end draw ID is more than 2 before last observation (6) and start is not at earliest (1)
140 | }
141 |
142 | function testGetDisbursedBetween_binarySearchBothStartAndEnd_2ndScenario() public {
143 | // here we want to test a case where the algorithm must binary search both the start and end observations
144 | add(1); // 1000
145 | add(2); // 1000 + 900
146 | add(3); // 1000 + 900 + 810
147 | add(4); // 1000 + 900 + 810 + 729
148 | add(5);
149 | add(6);
150 | assertApproxEqAbs(getDisbursedBetween(2, 4), 3e5, 1); // end draw ID is more than 2 before last observation (6) and start is not at earliest (1)
151 | }
152 |
153 | function testGetDisbursedBetween_binarySearchBothStartAndEnd_3rdScenario() public {
154 | // here we want to test a case where the algorithm must binary search both the start and end observations
155 | add(1); // 1000
156 | add(2); // 1000 + 900
157 | add(3); // 1000 + 900 + 810
158 | add(4); // 1000 + 900 + 810 + 729
159 | add(5); // 1000 + 900 + 810 + 729 + 656
160 | add(6);
161 | add(7);
162 | assertApproxEqAbs(getDisbursedBetween(2, 5), 4e5, 2); // end draw ID is more than 2 before last observation (6) and start is not at earliest (1)
163 | }
164 |
165 | function testGetDisbursedBetween_binarySearchBothStartAndEnd_4thScenario() public {
166 | // here we want to test a case where the algorithm must binary search both the start and end observations
167 | add(1); // 1000
168 | add(2); // 1000 + 900
169 | // 900 + 810
170 | // 810 + 729
171 | add(5); // 1000 + 729 + 656
172 | add(6);
173 | add(7);
174 | assertApproxEqAbs(getDisbursedBetween(2, 5), 2e5, 2); // end draw ID is more than 2 before last observation (6) and start is not at earliest (1)
175 | }
176 |
177 | function testGetDisbursedBetween_onOne() public {
178 | add(1);
179 | /*
180 | should include draw 1, 2, 3 and 4:
181 | 1 1000
182 | 2 900
183 | 3 810
184 | 4 729
185 | */
186 | assertEq(getDisbursedBetween(1, 4), 1e5);
187 | }
188 |
189 | function testGetDisbursedBetween_beforeOne() public {
190 | add(4);
191 | // should include draw 2, 3 and 4
192 | assertEq(getDisbursedBetween(2, 3), 0);
193 | }
194 |
195 | function testGetDisbursedBetween_endOnOne() public {
196 | add(4);
197 | // should include draw 2, 3 and 4
198 | assertEq(getDisbursedBetween(2, 4), 1e5);
199 | }
200 |
201 | function testGetDisbursedBetween_startOnOne() public {
202 | add(4);
203 | // should include draw 2, 3 and 4
204 | assertEq(getDisbursedBetween(4, 4), 1e5);
205 | }
206 |
207 | function testGetDisbursedBetween_afterOne() public {
208 | add(1);
209 | // should include draw 2, 3 and 4
210 | assertEq(getDisbursedBetween(2, 4), 0);
211 | }
212 |
213 | function testGetDisbursedBetween_beforeOnTwo() public {
214 | add(4);
215 | add(5);
216 | /*
217 | should include draw 1, 2, 3 and 4:
218 | 1 1000
219 | 2 900
220 | 3 810 + 1000
221 | 4 729 + 900
222 | */
223 | assertEq(getDisbursedBetween(1, 4), 1e5);
224 | }
225 |
226 | function testGetDisbursedBetween_aroundFirstOfTwo() public {
227 | add(4);
228 | add(6);
229 | /*
230 | should include draw 1, 2, 3 and 4:
231 | 1 1000
232 | 2 900
233 | 3 810 + 1000
234 | 4 729 + 900
235 | */
236 | assertEq(getDisbursedBetween(1, 5), 1e5);
237 | }
238 |
239 | function testGetDisbursedBetween_acrossTwo() public {
240 | add(2);
241 | add(4);
242 | /*
243 | should include draw 2, 3 and 4:
244 | 2 1000
245 | 3 900
246 | 4 810 + 1000
247 | 5 729 + 900
248 | */
249 | assertEq(getDisbursedBetween(1, 4), 2e5);
250 | }
251 |
252 | function testGetDisbursedBetween_onOneBetweenTwo() public {
253 | add(2);
254 | add(4);
255 | /*
256 | should include draw 2, 3 and 4:
257 | 2 1000
258 | 3 900
259 | 4 810 + 1000
260 | 5 729 + 900
261 | */
262 | assertEq(getDisbursedBetween(3, 3), 0);
263 | }
264 |
265 | function testGetDisbursedBetween_betweenTwo() public {
266 | add(1);
267 | add(4);
268 | /*
269 | should include draw 1, 2, 3 and 4:
270 | 1 1000
271 | 2 900
272 | 3 810
273 | 4 729 + 1000
274 | */
275 | assertEq(getDisbursedBetween(2, 3), 0);
276 | }
277 |
278 | function testGetDisbursedBetween_aroundLastOfTwo() public {
279 | add(1);
280 | add(4);
281 | /*
282 | should include draw 1, 2, 3 and 4:
283 | 1 1000
284 | 2 900
285 | 3 810
286 | 4 729 + 1000
287 | */
288 | assertEq(getDisbursedBetween(3, 4), 1e5);
289 | }
290 |
291 | function testGetDisbursedBetween_AfterLast() public {
292 | add(1);
293 | add(4);
294 | // 1 1000
295 | // 2 900
296 | // 3 810
297 | // 4 729 + 1000
298 | // 5 656 + 900
299 | // 6 590 + 810
300 | assertEq(getDisbursedBetween(6, 6), 0);
301 | }
302 |
303 | function testBinarySearchTwoWithFirstMatchingTarget() public {
304 | fillDrawRingBuffer([1, 3, 0, 0, 0]);
305 | (
306 | uint16 beforeOrAtIndex,
307 | uint24 beforeOrAtDrawId,
308 | uint16 afterOrAtIndex,
309 | uint24 afterOrAtDrawId
310 | ) = wrapper.binarySearch(0, 2, 2, 1);
311 | assertEq(beforeOrAtIndex, 0);
312 | assertEq(beforeOrAtDrawId, 1);
313 | assertEq(afterOrAtIndex, 1);
314 | assertEq(afterOrAtDrawId, 3);
315 | }
316 |
317 | function testBinarySearchMatchingTarget() public {
318 | fillDrawRingBuffer([1, 2, 3, 4, 5]);
319 | (
320 | uint16 beforeOrAtIndex,
321 | uint24 beforeOrAtDrawId,
322 | uint16 afterOrAtIndex,
323 | uint24 afterOrAtDrawId
324 | ) = wrapper.binarySearch(0, 4, 5, 3);
325 | assertEq(beforeOrAtIndex, 2);
326 | assertEq(beforeOrAtDrawId, 3);
327 | assertEq(afterOrAtIndex, 3);
328 | assertEq(afterOrAtDrawId, 4);
329 | }
330 |
331 | function testBinarySearchFirstMatchingTarget() public {
332 | fillDrawRingBuffer([1, 2, 3, 4, 5]);
333 | (
334 | uint16 beforeOrAtIndex,
335 | uint24 beforeOrAtDrawId,
336 | uint16 afterOrAtIndex,
337 | uint24 afterOrAtDrawId
338 | ) = wrapper.binarySearch(0, 4, 5, 1);
339 | assertEq(beforeOrAtIndex, 0);
340 | assertEq(beforeOrAtDrawId, 1);
341 | assertEq(afterOrAtIndex, 1);
342 | assertEq(afterOrAtDrawId, 2);
343 | }
344 |
345 | function testBinarySearchLastMatchingTarget() public {
346 | fillDrawRingBuffer([1, 2, 3, 4, 5]);
347 | (
348 | uint16 beforeOrAtIndex,
349 | uint24 beforeOrAtDrawId,
350 | uint16 afterOrAtIndex,
351 | uint24 afterOrAtDrawId
352 | ) = wrapper.binarySearch(0, 4, 5, 5);
353 | assertEq(beforeOrAtIndex, 3);
354 | assertEq(beforeOrAtDrawId, 4);
355 | assertEq(afterOrAtIndex, 4);
356 | assertEq(afterOrAtDrawId, 5);
357 | }
358 |
359 | function testBinarySearchTargetBetween() public {
360 | fillDrawRingBuffer([2, 4, 5, 6, 7]);
361 | (
362 | uint16 beforeOrAtIndex,
363 | uint24 beforeOrAtDrawId,
364 | uint16 afterOrAtIndex,
365 | uint24 afterOrAtDrawId
366 | ) = wrapper.binarySearch(0, 4, 5, 3);
367 | assertEq(beforeOrAtIndex, 0);
368 | assertEq(beforeOrAtDrawId, 2);
369 | assertEq(afterOrAtIndex, 1);
370 | assertEq(afterOrAtDrawId, 4);
371 | }
372 |
373 | function fillDrawRingBuffer(uint8[5] memory values) internal {
374 | for (uint16 i = 0; i < values.length; i++) {
375 | wrapper.setDrawRingBuffer(i, values[i]);
376 | }
377 | wrapper.setRingBufferInfo(uint16(values.length), uint16(values.length));
378 | }
379 |
380 | function add(uint16 drawId) internal {
381 | wrapper.add(1e5, drawId);
382 | }
383 |
384 | function getDisbursedBetween(
385 | uint16 _startDrawId,
386 | uint16 _endDrawId
387 | ) internal view returns (uint256) {
388 | return wrapper.getDisbursedBetween(_startDrawId, _endDrawId);
389 | }
390 | }
391 |
--------------------------------------------------------------------------------
/test/abstract/TieredLiquidityDistributor.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/Test.sol";
5 |
6 | import { TierCalculationLib } from "../../src/libraries/TierCalculationLib.sol";
7 | import { TieredLiquidityDistributorWrapper } from "./helper/TieredLiquidityDistributorWrapper.sol";
8 | import {
9 | UD60x18,
10 | NumberOfTiersLessThanMinimum,
11 | NumberOfTiersGreaterThanMaximum,
12 | TierLiquidityUtilizationRateGreaterThanOne,
13 | TierLiquidityUtilizationRateCannotBeZero,
14 | InsufficientLiquidity,
15 | convert,
16 | SD59x18,
17 | sd,
18 | MAXIMUM_NUMBER_OF_TIERS,
19 | MINIMUM_NUMBER_OF_TIERS,
20 | NUMBER_OF_CANARY_TIERS
21 | } from "../../src/abstract/TieredLiquidityDistributor.sol";
22 |
23 | contract TieredLiquidityDistributorTest is Test {
24 |
25 | event ReserveConsumed(uint256 amount);
26 |
27 | TieredLiquidityDistributorWrapper public distributor;
28 |
29 | uint24 grandPrizePeriodDraws;
30 | uint8 numberOfTiers;
31 | uint8 tierShares;
32 | uint8 canaryShares;
33 | uint8 reserveShares;
34 | uint256 tierLiquidityUtilizationRate;
35 |
36 | function setUp() external {
37 | numberOfTiers = MINIMUM_NUMBER_OF_TIERS;
38 | tierShares = 100;
39 | canaryShares = 5;
40 | reserveShares = 10;
41 | grandPrizePeriodDraws = 365;
42 | tierLiquidityUtilizationRate = 1e18;
43 |
44 | distributor = new TieredLiquidityDistributorWrapper(
45 | tierLiquidityUtilizationRate,
46 | numberOfTiers,
47 | tierShares,
48 | canaryShares,
49 | reserveShares,
50 | grandPrizePeriodDraws
51 | );
52 | }
53 |
54 | function testAwardDraw_invalid_num_tiers() public {
55 | vm.expectRevert(abi.encodeWithSelector(NumberOfTiersLessThanMinimum.selector, 1));
56 | distributor.awardDraw(1, 100);
57 | }
58 |
59 | function testAwardDraw() public {
60 | uint liq1 = 320e18;
61 | distributor.awardDraw(5, liq1);
62 | assertEq(distributor.getTierRemainingLiquidity(0), uint(tierShares) * 1e18, "tier 0 accrued fully");
63 | assertEq(distributor.getTierRemainingLiquidity(1), uint(tierShares) * 1e18, "daily tier");
64 | assertEq(distributor.getTierRemainingLiquidity(2), uint(tierShares) * 1e18, "canary accrued one draw 1");
65 | assertEq(distributor.getTierRemainingLiquidity(3), uint(canaryShares) * 1e18, "canary accrued one draw 2");
66 | assertEq(distributor.getTierRemainingLiquidity(4), uint(canaryShares) * 1e18, "reduced tier has nothing");
67 | assertEq(distributor.reserve(), uint(reserveShares) * 1e18, "reserve");
68 | assertEq(_computeLiquidity(), liq1, "total");
69 | }
70 |
71 | function testAwardDraw_liquidity_shrinkTiers1() public {
72 | uint liq1 = 320e18; //distributor.computeTotalShares(5) * 1e18; // 5 tiers => 3 normal, 2 canary. => 320e18 total funds.
73 | distributor.awardDraw(5, liq1);
74 |
75 | assertEq(distributor.getTierRemainingLiquidity(0), uint(tierShares) * 1e18, "tier 0");
76 | assertEq(distributor.getTierRemainingLiquidity(1), uint(tierShares) * 1e18, "tier 1");
77 | assertEq(distributor.getTierRemainingLiquidity(2), uint(tierShares) * 1e18, "daily tier");
78 | assertEq(distributor.getTierRemainingLiquidity(3), uint(canaryShares) * 1e18, "canary 1");
79 | assertEq(distributor.getTierRemainingLiquidity(4), uint(canaryShares) * 1e18, "canary 2");
80 | assertEq(distributor.getTierRemainingLiquidity(5), 0, "nothing beyond");
81 |
82 | // we are shrinking, so we'll recoup the two canaries = 110e18
83 | uint liq2 = 110e18;
84 | distributor.awardDraw(4, liq2);
85 | assertEq(distributor.getTierRemainingLiquidity(0), uint(tierShares) * 2e18, "tier 0 accrued fully");
86 | assertEq(distributor.getTierRemainingLiquidity(1), uint(tierShares) * 2e18, "daily tier");
87 | assertEq(distributor.getTierRemainingLiquidity(2), uint(canaryShares) * 1e18, "canary accrued one draw 1");
88 | assertEq(distributor.getTierRemainingLiquidity(3), uint(canaryShares) * 1e18, "canary accrued one draw 2");
89 | assertEq(distributor.getTierRemainingLiquidity(4), 0, "reduced tier has nothing");
90 | assertEq(distributor.reserve(), uint(reserveShares) * 2e18, "reserve");
91 | assertEq(_computeLiquidity(), liq1 + liq2, "total");
92 | }
93 |
94 | function testAwardDraw_liquidity_shrinkTiers2() public {
95 | distributor.awardDraw(7, 520e18); // 5 normal, 2 canary and reserve
96 | // reclaim 2 canary and 2 regs => 210e18 reclaimed
97 | distributor.awardDraw(5, 110e18); // 3 normal, 2 canary
98 |
99 | assertEq(distributor.getTierRemainingLiquidity(0), 200e18, "tier 0");
100 | assertEq(distributor.getTierRemainingLiquidity(1), 200e18, "tier 1");
101 | assertEq(distributor.getTierRemainingLiquidity(2), 200e18, "daily");
102 | assertEq(distributor.getTierRemainingLiquidity(3), 5e18, "canary 1");
103 | assertEq(distributor.getTierRemainingLiquidity(4), 5e18, "canary 2");
104 | assertEq(distributor.getTierRemainingLiquidity(5), 0);
105 | assertEq(distributor.reserve(), 20e18, "reserve");
106 |
107 | assertEq(_computeLiquidity(), 630e18, "total liquidity");
108 | }
109 |
110 | function testAwardDraw_liquidity_sameTiers() public {
111 | distributor.awardDraw(5, 100e18);
112 | distributor.awardDraw(5, 100e18);
113 | assertEq(_computeLiquidity(), 200e18);
114 | }
115 |
116 | function testAwardDraw_liquidity_growTiers1() public {
117 | distributor.awardDraw(5, 320e18);
118 | assertEq(_computeLiquidity(), 320e18, "total liquidity for first draw");
119 | distributor.awardDraw(6, 420e18);
120 | assertEq(_computeLiquidity(), 740e18, "total liquidity for second draw");
121 | }
122 |
123 | function testAwardDraw_liquidity_growTiers2() public {
124 | distributor.awardDraw(5, 320e18); // 3 tiers and 2 canary. 3 tiers stay, canaries reclaimed.
125 | // reclaimed 10e18
126 | distributor.awardDraw(7, 510e18); // 5 tiers and 2 canary
127 |
128 | assertEq(distributor.getTierRemainingLiquidity(0), 200e18, "old tier 0 continues to accrue");
129 | assertEq(distributor.getTierRemainingLiquidity(1), 200e18, "old tier 1 continues to accrue");
130 | assertEq(distributor.getTierRemainingLiquidity(2), 200e18, "old tier 2 continues to accrue");
131 | assertEq(distributor.getTierRemainingLiquidity(3), 100e18, "old tier 3 continues to accrue");
132 | assertEq(distributor.getTierRemainingLiquidity(4), 100e18, "old canary gets reclaimed");
133 | assertEq(distributor.getTierRemainingLiquidity(5), 5e18, "new tier who dis 1");
134 | assertEq(distributor.getTierRemainingLiquidity(6), 5e18, "new tier who dis 2");
135 | assertEq(distributor.reserve(), 20e18, "reserve");
136 | assertEq(_computeLiquidity(), 830e18, "total liquidity");
137 | }
138 |
139 | function testConstructor_numberOfTiersTooLarge() public {
140 | vm.expectRevert(abi.encodeWithSelector(NumberOfTiersGreaterThanMaximum.selector, 16));
141 | new TieredLiquidityDistributorWrapper(tierLiquidityUtilizationRate, 16, tierShares, canaryShares, reserveShares, 365);
142 | }
143 |
144 | function testConstructor_numberOfTiersTooSmall() public {
145 | vm.expectRevert(abi.encodeWithSelector(NumberOfTiersLessThanMinimum.selector, 1));
146 | new TieredLiquidityDistributorWrapper(tierLiquidityUtilizationRate, 1, tierShares, canaryShares, reserveShares, 365);
147 | }
148 |
149 | function testConstructor_tierLiquidityUtilizationRate_gt_1() public {
150 | vm.expectRevert(abi.encodeWithSelector(TierLiquidityUtilizationRateGreaterThanOne.selector));
151 | new TieredLiquidityDistributorWrapper(1e18 + 1, MINIMUM_NUMBER_OF_TIERS, tierShares, canaryShares, reserveShares, 365);
152 | }
153 |
154 | function testConstructor_tierLiquidityUtilizationRate_zero() public {
155 | vm.expectRevert(abi.encodeWithSelector(TierLiquidityUtilizationRateCannotBeZero.selector));
156 | new TieredLiquidityDistributorWrapper(0, MINIMUM_NUMBER_OF_TIERS, tierShares, canaryShares, reserveShares, 365);
157 | }
158 |
159 | function testRemainingTierLiquidity() public {
160 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, distributor.getTotalShares() * 1e18);
161 | assertEq(distributor.getTierRemainingLiquidity(0), 100e18, "tier 0");
162 | assertEq(distributor.getTierRemainingLiquidity(1), 100e18, "tier 1");
163 | assertEq(distributor.getTierRemainingLiquidity(2), 5e18, "tier 2");
164 | assertEq(distributor.getTierRemainingLiquidity(3), 5e18, "tier 3");
165 | }
166 |
167 | // regression test to see if there are any unaccounted rounding errors on consumeLiquidity
168 | function testConsumeLiquidity_roundingErrors() public {
169 | distributor = new TieredLiquidityDistributorWrapper(
170 | tierLiquidityUtilizationRate,
171 | numberOfTiers,
172 | 100,
173 | 9,
174 | 0,
175 | grandPrizePeriodDraws
176 | );
177 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 218e18); // 100 for each tier + 9 for each canary
178 |
179 | uint256 reserveBefore = distributor.reserve();
180 |
181 | // There is 9e18 liquidity available for tier 3.
182 | // Each time we consume 1 liquidity we will lose 0.00001 to rounding errors in
183 | // the tier.prizeTokenPerShare value. Over time, this will accumulate and lead
184 | // to the tier thinking is has more remainingLiquidity than it actually does.
185 | for (uint i = 1; i <= 10000; i++) {
186 | distributor.consumeLiquidity(3, 1);
187 | assertEq(distributor.getTierRemainingLiquidity(3) + (distributor.reserve() - reserveBefore), 9e18 - i);
188 | }
189 |
190 | // Test that we can still consume the rest of the liquidity even it if dips in the reserve
191 | assertEq(distributor.getTierRemainingLiquidity(3), 9e18 - 90000); // 10000 consumed + 10000 rounding errors, rounding up by 8 each time
192 | assertEq(distributor.reserve(), 80000);
193 | vm.expectEmit();
194 | emit ReserveConsumed(80000); // equal to the rounding errors (8 for each one)
195 | distributor.consumeLiquidity(3, 9e18 - 10000); // we only consumed 10000, so we should still be able to consume the rest by dipping into reserve
196 | assertEq(distributor.getTierRemainingLiquidity(3), 0);
197 | }
198 |
199 | function testConsumeLiquidity_partial() public {
200 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
201 | distributor.consumeLiquidity(1, 50e18); // consume full liq for tier 1
202 | assertEq(distributor.getTierRemainingLiquidity(1), 50e18);
203 | }
204 |
205 | function testConsumeLiquidity_full() public {
206 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
207 | distributor.consumeLiquidity(1, 100e18); // consume full liq for tier 1
208 | assertEq(distributor.getTierRemainingLiquidity(1), 0);
209 | }
210 |
211 | function testConsumeLiquidity_and_empty_reserve() public {
212 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18); // reserve should be 10e18
213 | uint256 reserve = distributor.reserve();
214 | distributor.consumeLiquidity(1, 110e18); // consume full liq for tier 1 and reserve
215 | assertEq(distributor.getTierRemainingLiquidity(1), 0);
216 | assertEq(distributor.reserve(), 0);
217 | assertLt(distributor.reserve(), reserve);
218 | }
219 |
220 | function testConsumeLiquidity_and_partial_reserve() public {
221 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18); // reserve should be 10e18
222 | uint256 reserve = distributor.reserve();
223 | distributor.consumeLiquidity(1, 105e18); // consume full liq for tier 1 and reserve
224 | assertEq(distributor.getTierRemainingLiquidity(1), 0);
225 | assertEq(distributor.reserve(), 5e18);
226 | assertLt(distributor.reserve(), reserve);
227 | }
228 |
229 | function testConsumeLiquidity_insufficient() public {
230 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18); // reserve should be 10e18
231 | vm.expectRevert(abi.encodeWithSelector(InsufficientLiquidity.selector, 120e18));
232 | distributor.consumeLiquidity(1, 120e18);
233 | }
234 |
235 | function testGetTierPrizeSize_noDraw() public {
236 | assertEq(distributor.getTierPrizeSize(4), 0);
237 | }
238 |
239 | function testGetTierPrizeSize_invalid() public {
240 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
241 | assertEq(distributor.getTierPrizeSize(4), 0);
242 | }
243 |
244 | function testGetTierPrizeSize_grandPrize() public {
245 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
246 | assertEq(distributor.getTierPrizeSize(0), 100e18);
247 | }
248 |
249 | function testGetTierPrizeSize_grandPrize_utilizationLower() public {
250 | tierLiquidityUtilizationRate = 0.5e18;
251 | distributor = new TieredLiquidityDistributorWrapper(tierLiquidityUtilizationRate, numberOfTiers, tierShares, canaryShares, reserveShares, grandPrizePeriodDraws);
252 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
253 | assertEq(distributor.getTierPrizeSize(0), 50e18);
254 | assertEq(distributor.getTierRemainingLiquidity(0), 100e18);
255 | }
256 |
257 | function testGetTierPrizeSize_overflow() public {
258 | distributor = new TieredLiquidityDistributorWrapper(tierLiquidityUtilizationRate, numberOfTiers, tierShares, canaryShares, 0, 365);
259 |
260 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, type(uint104).max);
261 | distributor.awardDraw(4, type(uint104).max);
262 | distributor.awardDraw(5, type(uint104).max);
263 | distributor.awardDraw(6, type(uint104).max);
264 |
265 | assertEq(distributor.getTierPrizeSize(0), type(uint104).max);
266 | }
267 |
268 | function testGetRemainingTierLiquidity() public {
269 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
270 | assertEq(distributor.getTierRemainingLiquidity(0), 100e18);
271 | }
272 |
273 | function testGetTierRemainingLiquidity() public {
274 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
275 | assertEq(distributor.getTierRemainingLiquidity(0), 100e18);
276 | }
277 |
278 | function testGetTierRemainingLiquidity_invalid() public {
279 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, 220e18);
280 | assertEq(distributor.getTierRemainingLiquidity(5), 0);
281 | }
282 |
283 | function testIsCanaryTier() public {
284 | assertEq(distributor.isCanaryTier(0), false, "grand prize");
285 | assertEq(distributor.isCanaryTier(1), false, "daily tier");
286 | assertEq(distributor.isCanaryTier(2), true, "canary 1");
287 | assertEq(distributor.isCanaryTier(3), true, "canary 2");
288 | }
289 |
290 | function testExpansionTierLiquidity() public {
291 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, distributor.getTotalShares() * 1e18); // canary gets 100e18
292 | assertEq(distributor.getTierRemainingLiquidity(0), 100e18, "grand prize liquidity");
293 | assertEq(distributor.getTierRemainingLiquidity(1), 100e18, "tier 1 liquidity");
294 | assertEq(distributor.getTierRemainingLiquidity(2), 5e18, "canary 1 liquidity");
295 | assertEq(distributor.getTierRemainingLiquidity(3), 5e18, "canary 2 liquidity");
296 | assertEq(distributor.reserve(), 10e18, "reserve liquidity");
297 |
298 | // canary will be reclaimed
299 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS+1, 310e18); // total will be 200e18 + 210e18 = 410e18
300 |
301 | assertEq(distributor.getTierRemainingLiquidity(0), 200e18, "grand prize liquidity");
302 | assertEq(distributor.getTierRemainingLiquidity(1), 200e18, "tier 1 liquidity");
303 | assertEq(distributor.getTierRemainingLiquidity(2), 100e18, "tier 2 liquidity");
304 | assertEq(distributor.getTierRemainingLiquidity(3), 5e18, "tier 3 liquidity");
305 | assertEq(distributor.getTierRemainingLiquidity(4), 5e18, "last tier out");
306 | }
307 |
308 | function testExpansionTierLiquidity_max() public {
309 | uint96 amount = 79228162514264337593543950333;
310 | // uint96 amount = 100e18;
311 | distributor.awardDraw(15, amount);
312 |
313 | uint128 prizeTokenPerShare = distributor.prizeTokenPerShare();
314 | uint256 total = (
315 | prizeTokenPerShare * (distributor.getTotalShares() - distributor.reserveShares())
316 | ) + distributor.reserve();
317 | assertEq(total, amount, "prize token per share against total shares");
318 |
319 | uint256 summed;
320 | for (uint8 t = 0; t < distributor.numberOfTiers(); t++) {
321 | summed += distributor.getTierRemainingLiquidity(t);
322 | }
323 | summed += distributor.reserve();
324 |
325 | assertEq(summed, amount, "summed amount across prize tiers");
326 | }
327 |
328 | function testGetTierOdds_AllAvailable() public {
329 | SD59x18 odds;
330 | grandPrizePeriodDraws = distributor.grandPrizePeriodDraws();
331 | for (
332 | uint8 numTiers = MINIMUM_NUMBER_OF_TIERS;
333 | numTiers <= MAXIMUM_NUMBER_OF_TIERS;
334 | numTiers++
335 | ) {
336 | for (uint8 tier = 0; tier < numTiers; tier++) {
337 | odds = distributor.getTierOdds(tier, numTiers);
338 | }
339 | }
340 | }
341 |
342 | function testGetTierPrizeCount() public {
343 | assertEq(distributor.getTierPrizeCount(0), 1);
344 | assertEq(distributor.getTierPrizeCount(1), 4);
345 | assertEq(distributor.getTierPrizeCount(2), 16);
346 | }
347 |
348 | function testGetTierOdds_grandPrize() public {
349 | for (uint8 i = MINIMUM_NUMBER_OF_TIERS; i <= MAXIMUM_NUMBER_OF_TIERS; i++) {
350 | assertEq(distributor.getTierOdds(0, i).unwrap(), int(1e18)/int(365), string.concat("grand for num tiers ", string(abi.encode(i))));
351 | }
352 | }
353 |
354 | function testGetTierOdds_dailyCanary() public {
355 | // 3 - 10
356 | for (uint8 i = MINIMUM_NUMBER_OF_TIERS - 1; i <= MAXIMUM_NUMBER_OF_TIERS; i++) {
357 | // last tier (canary 2)
358 | assertEq(distributor.getTierOdds(i-1, i).unwrap(), 1e18, string.concat("canary 2 for num tiers ", string(abi.encode(i))));
359 | // third to last (daily)
360 | assertEq(distributor.getTierOdds(i-2, i).unwrap(), 1e18, string.concat("canary 1 for num tiers ", string(abi.encode(i))));
361 | if (i > 3) {
362 | // second to last tier (canary 1)
363 | assertEq(distributor.getTierOdds(i-3, i).unwrap(), 1e18, string.concat("daily for num tiers ", string(abi.encode(i))));
364 | }
365 | }
366 | }
367 |
368 | function testGetTierOdds_zero_when_outside_bounds() public {
369 | SD59x18 odds;
370 | for (
371 | uint8 numTiers = MINIMUM_NUMBER_OF_TIERS - 1;
372 | numTiers <= MAXIMUM_NUMBER_OF_TIERS;
373 | numTiers++
374 | ) {
375 | odds = distributor.getTierOdds(numTiers, numTiers);
376 | assertEq(SD59x18.unwrap(odds), 0);
377 | }
378 | }
379 |
380 | function testEstimateNumberOfTiersUsingPrizeCountPerDraw_allTiers() public {
381 | uint32 prizeCount;
382 | for (
383 | uint8 numTiers = MINIMUM_NUMBER_OF_TIERS;
384 | numTiers <= MAXIMUM_NUMBER_OF_TIERS;
385 | numTiers++
386 | ) {
387 | prizeCount = distributor.estimatedPrizeCount(numTiers);
388 | assertEq(
389 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(prizeCount - 1),
390 | numTiers,
391 | "slightly under"
392 | );
393 | assertEq(
394 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(prizeCount),
395 | numTiers,
396 | "match"
397 | );
398 | assertEq(
399 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(prizeCount + 1),
400 | numTiers,
401 | "slightly over"
402 | );
403 | }
404 |
405 | assertEq(distributor.estimatedPrizeCount(12), 0, "exceeds bounds");
406 | }
407 |
408 | function testEstimateNumberOfTiersUsingPrizeCountPerDraw_loose() public {
409 | // 270 prizes for num tiers = 5
410 | assertEq(
411 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(250),
412 | 6,
413 | "matches slightly under"
414 | );
415 | assertEq(
416 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(270),
417 | 6,
418 | "matches exact"
419 | );
420 | assertEq(
421 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(280),
422 | 6,
423 | "matches slightly over"
424 | );
425 | assertEq(
426 | distributor.estimateNumberOfTiersUsingPrizeCountPerDraw(540),
427 | 6,
428 | "matches significantly over"
429 | );
430 | }
431 |
432 | function testEstimatedPrizeCount_noParam() public {
433 | assertEq(distributor.estimatedPrizeCount(), 20);
434 | }
435 |
436 | function testEstimatedPrizeCount_allTiers() public {
437 | assertEq(distributor.estimatedPrizeCount(4), 20, "num tiers 4");
438 | assertEq(distributor.estimatedPrizeCount(5), 80, "num tiers 5");
439 | assertEq(distributor.estimatedPrizeCount(6), 320, "num tiers 6");
440 | assertEq(distributor.estimatedPrizeCount(7), 1283, "num tiers 7");
441 | assertEq(distributor.estimatedPrizeCount(8), 5139, "num tiers 8");
442 | assertEq(distributor.estimatedPrizeCount(9), 20580, "num tiers 9");
443 | assertEq(distributor.estimatedPrizeCount(10), 82408, "num tiers 10");
444 | assertEq(distributor.estimatedPrizeCount(11), 329958, "num tiers 11");
445 | assertEq(distributor.estimatedPrizeCount(12), 0, "num tiers 12");
446 | }
447 |
448 | function testEstimatedPrizeCountWithBothCanaries_allTiers() public {
449 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(3), 0, "num tiers 3");
450 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(4), 20 + 4**3, "num tiers 4");
451 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(5), 80 + 4**4, "num tiers 5");
452 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(6), 320 + 4**5, "num tiers 6");
453 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(7), 1283 + 4**6, "num tiers 7");
454 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(12), 0, "num tiers 12");
455 | }
456 |
457 | function testEstimatedPrizeCountWithBothCanaries() public {
458 | assertEq(distributor.estimatedPrizeCountWithBothCanaries(), 20 + 4**3, "num tiers 4");
459 | }
460 |
461 | function testSumTierPrizeCounts() public {
462 | // 16 canary 1 daily + 64 canary 2 daily = 80
463 | assertEq(distributor.sumTierPrizeCounts(5), 80, "num tiers 5");
464 | // 64 + 256 = ~320
465 | assertEq(distributor.sumTierPrizeCounts(6), 320, "num tiers 6");
466 | // 256 + 1024 = ~1280
467 | assertEq(distributor.sumTierPrizeCounts(7), 1283, "num tiers 7");
468 | // 1024 + 4096 = ~5120 (plus a few prizes from non-daily tiers)
469 | assertEq(distributor.sumTierPrizeCounts(8), 5139, "num tiers 8");
470 | assertEq(distributor.sumTierPrizeCounts(9), 20580, "num tiers 9");
471 | assertEq(distributor.sumTierPrizeCounts(10), 82408, "num tiers 10");
472 | assertEq(distributor.sumTierPrizeCounts(11), 329958, "num tiers 11");
473 | assertEq(distributor.sumTierPrizeCounts(12), 0, "num tiers 12");
474 | }
475 |
476 | function testExpansionTierLiquidity_regression() public {
477 | uint96 amount1 = 253012247290373118207;
478 | uint96 amount2 = 99152290762372054017;
479 | uint96 amount3 = 79228162514264337593543950333;
480 | uint total = amount1 + amount2 + uint(amount3);
481 |
482 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, amount1);
483 |
484 | assertEq(summedLiquidity(), amount1, "after amount1");
485 |
486 | distributor.awardDraw(MINIMUM_NUMBER_OF_TIERS, amount2);
487 |
488 | assertEq(summedLiquidity(), amount1 + uint(amount2), "after amount2");
489 |
490 | distributor.awardDraw(15, amount3);
491 |
492 | assertEq(summedLiquidity(), total, "after amount3");
493 | }
494 |
495 | function summedLiquidity() public view returns (uint256) {
496 | uint256 summed;
497 | for (uint8 t = 0; t < distributor.numberOfTiers(); t++) {
498 | summed += distributor.getTierRemainingLiquidity(t);
499 | }
500 | summed += distributor.reserve();
501 | return summed;
502 | }
503 |
504 | function _computeLiquidity() internal view returns (uint256) {
505 | // console2.log("test _computeLiquidity, distributor.numberOfTiers()", distributor.numberOfTiers());
506 | uint256 liquidity = _getTotalTierRemainingLiquidity(distributor.numberOfTiers());
507 | liquidity += distributor.reserve();
508 | return liquidity;
509 | }
510 |
511 | function _getTotalTierRemainingLiquidity(uint8 _numberOfTiers) internal view returns (uint256) {
512 | uint256 liquidity = 0;
513 | for (uint8 i = 0; i < _numberOfTiers; i++) {
514 | liquidity += distributor.getTierRemainingLiquidity(i);
515 | }
516 | return liquidity;
517 | }
518 | }
519 |
--------------------------------------------------------------------------------
/src/abstract/TieredLiquidityDistributor.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.19;
4 |
5 | import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
6 | import { SD59x18, sd } from "prb-math/SD59x18.sol";
7 | import { UD60x18, convert } from "prb-math/UD60x18.sol";
8 |
9 | import { TierCalculationLib } from "../libraries/TierCalculationLib.sol";
10 |
11 | /// @notice Struct that tracks tier liquidity information.
12 | /// @param drawId The draw ID that the tier was last updated for
13 | /// @param prizeSize The size of the prize for the tier at the drawId
14 | /// @param prizeTokenPerShare The total prize tokens per share that have already been consumed for this tier.
15 | struct Tier {
16 | uint24 drawId;
17 | uint104 prizeSize;
18 | uint128 prizeTokenPerShare;
19 | }
20 |
21 | /// @notice Thrown when the number of tiers is less than the minimum number of tiers.
22 | /// @param numTiers The invalid number of tiers
23 | error NumberOfTiersLessThanMinimum(uint8 numTiers);
24 |
25 | /// @notice Thrown when the number of tiers is greater than the max tiers
26 | /// @param numTiers The invalid number of tiers
27 | error NumberOfTiersGreaterThanMaximum(uint8 numTiers);
28 |
29 | /// @notice Thrown when the tier liquidity utilization rate is greater than 1.
30 | error TierLiquidityUtilizationRateGreaterThanOne();
31 |
32 | /// @notice Thrown when the tier liquidity utilization rate is 0.
33 | error TierLiquidityUtilizationRateCannotBeZero();
34 |
35 | /// @notice Thrown when there is insufficient liquidity to consume.
36 | /// @param requestedLiquidity The requested amount of liquidity
37 | error InsufficientLiquidity(uint104 requestedLiquidity);
38 |
39 | uint8 constant MINIMUM_NUMBER_OF_TIERS = 4;
40 | uint8 constant MAXIMUM_NUMBER_OF_TIERS = 11;
41 | uint8 constant NUMBER_OF_CANARY_TIERS = 2;
42 |
43 | /// @title Tiered Liquidity Distributor
44 | /// @author PoolTogether Inc.
45 | /// @notice A contract that distributes liquidity according to PoolTogether V5 distribution rules.
46 | contract TieredLiquidityDistributor {
47 | /* ============ Events ============ */
48 |
49 | /// @notice Emitted when the reserve is consumed due to insufficient prize liquidity.
50 | /// @param amount The amount to decrease by
51 | event ReserveConsumed(uint256 amount);
52 |
53 | /* ============ Constants ============ */
54 |
55 | /// @notice The odds for each tier and number of tiers pair. For n tiers, the last three tiers are always daily.
56 | SD59x18 internal immutable TIER_ODDS_0;
57 | SD59x18 internal immutable TIER_ODDS_EVERY_DRAW;
58 | SD59x18 internal immutable TIER_ODDS_1_5;
59 | SD59x18 internal immutable TIER_ODDS_1_6;
60 | SD59x18 internal immutable TIER_ODDS_2_6;
61 | SD59x18 internal immutable TIER_ODDS_1_7;
62 | SD59x18 internal immutable TIER_ODDS_2_7;
63 | SD59x18 internal immutable TIER_ODDS_3_7;
64 | SD59x18 internal immutable TIER_ODDS_1_8;
65 | SD59x18 internal immutable TIER_ODDS_2_8;
66 | SD59x18 internal immutable TIER_ODDS_3_8;
67 | SD59x18 internal immutable TIER_ODDS_4_8;
68 | SD59x18 internal immutable TIER_ODDS_1_9;
69 | SD59x18 internal immutable TIER_ODDS_2_9;
70 | SD59x18 internal immutable TIER_ODDS_3_9;
71 | SD59x18 internal immutable TIER_ODDS_4_9;
72 | SD59x18 internal immutable TIER_ODDS_5_9;
73 | SD59x18 internal immutable TIER_ODDS_1_10;
74 | SD59x18 internal immutable TIER_ODDS_2_10;
75 | SD59x18 internal immutable TIER_ODDS_3_10;
76 | SD59x18 internal immutable TIER_ODDS_4_10;
77 | SD59x18 internal immutable TIER_ODDS_5_10;
78 | SD59x18 internal immutable TIER_ODDS_6_10;
79 | SD59x18 internal immutable TIER_ODDS_1_11;
80 | SD59x18 internal immutable TIER_ODDS_2_11;
81 | SD59x18 internal immutable TIER_ODDS_3_11;
82 | SD59x18 internal immutable TIER_ODDS_4_11;
83 | SD59x18 internal immutable TIER_ODDS_5_11;
84 | SD59x18 internal immutable TIER_ODDS_6_11;
85 | SD59x18 internal immutable TIER_ODDS_7_11;
86 |
87 | /// @notice The estimated number of prizes given X tiers.
88 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS;
89 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS;
90 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS;
91 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS;
92 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS;
93 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS;
94 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS;
95 | uint32 internal immutable ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS;
96 |
97 | /// @notice The Tier liquidity data.
98 | mapping(uint8 tierId => Tier tierData) internal _tiers;
99 |
100 | /// @notice The frequency of the grand prize
101 | uint24 public immutable grandPrizePeriodDraws;
102 |
103 | /// @notice The number of shares to allocate to each prize tier.
104 | uint8 public immutable tierShares;
105 |
106 | /// @notice The number of shares to allocate to each canary tier.
107 | uint8 public immutable canaryShares;
108 |
109 | /// @notice The number of shares to allocate to the reserve.
110 | uint8 public immutable reserveShares;
111 |
112 | /// @notice The percentage of tier liquidity to target for utilization.
113 | UD60x18 public immutable tierLiquidityUtilizationRate;
114 |
115 | /// @notice The number of prize tokens that have accrued per share for all time.
116 | /// @dev This is an ever-increasing exchange rate that is used to calculate the prize liquidity for each tier.
117 | /// @dev Each tier holds a separate tierPrizeTokenPerShare; the delta between the tierPrizeTokenPerShare and
118 | /// the prizeTokenPerShare * tierShares is the available liquidity they have.
119 | uint128 public prizeTokenPerShare;
120 |
121 | /// @notice The number of tiers for the last awarded draw. The last tier is the canary tier.
122 | uint8 public numberOfTiers;
123 |
124 | /// @notice The draw id of the last awarded draw.
125 | uint24 internal _lastAwardedDrawId;
126 |
127 | /// @notice The timestamp at which the last awarded draw was awarded.
128 | uint48 public lastAwardedDrawAwardedAt;
129 |
130 | /// @notice The amount of available reserve.
131 | uint96 internal _reserve;
132 |
133 | /// @notice Constructs a new Prize Pool.
134 | /// @param _tierLiquidityUtilizationRate The target percentage of tier liquidity to utilize each draw
135 | /// @param _numberOfTiers The number of tiers to start with. Must be greater than or equal to the minimum number of tiers.
136 | /// @param _tierShares The number of shares to allocate to each tier
137 | /// @param _canaryShares The number of shares to allocate to each canary tier
138 | /// @param _reserveShares The number of shares to allocate to the reserve.
139 | /// @param _grandPrizePeriodDraws The number of draws between grand prizes
140 | constructor(
141 | uint256 _tierLiquidityUtilizationRate,
142 | uint8 _numberOfTiers,
143 | uint8 _tierShares,
144 | uint8 _canaryShares,
145 | uint8 _reserveShares,
146 | uint24 _grandPrizePeriodDraws
147 | ) {
148 | if (_numberOfTiers < MINIMUM_NUMBER_OF_TIERS) {
149 | revert NumberOfTiersLessThanMinimum(_numberOfTiers);
150 | }
151 | if (_numberOfTiers > MAXIMUM_NUMBER_OF_TIERS) {
152 | revert NumberOfTiersGreaterThanMaximum(_numberOfTiers);
153 | }
154 | if (_tierLiquidityUtilizationRate > 1e18) {
155 | revert TierLiquidityUtilizationRateGreaterThanOne();
156 | }
157 | if (_tierLiquidityUtilizationRate == 0) {
158 | revert TierLiquidityUtilizationRateCannotBeZero();
159 | }
160 |
161 | tierLiquidityUtilizationRate = UD60x18.wrap(_tierLiquidityUtilizationRate);
162 |
163 | numberOfTiers = _numberOfTiers;
164 | tierShares = _tierShares;
165 | canaryShares = _canaryShares;
166 | reserveShares = _reserveShares;
167 | grandPrizePeriodDraws = _grandPrizePeriodDraws;
168 |
169 | TIER_ODDS_0 = sd(1).div(sd(int24(_grandPrizePeriodDraws)));
170 | TIER_ODDS_EVERY_DRAW = SD59x18.wrap(1000000000000000000);
171 | TIER_ODDS_1_5 = TierCalculationLib.getTierOdds(1, 3, _grandPrizePeriodDraws);
172 | TIER_ODDS_1_6 = TierCalculationLib.getTierOdds(1, 4, _grandPrizePeriodDraws);
173 | TIER_ODDS_2_6 = TierCalculationLib.getTierOdds(2, 4, _grandPrizePeriodDraws);
174 | TIER_ODDS_1_7 = TierCalculationLib.getTierOdds(1, 5, _grandPrizePeriodDraws);
175 | TIER_ODDS_2_7 = TierCalculationLib.getTierOdds(2, 5, _grandPrizePeriodDraws);
176 | TIER_ODDS_3_7 = TierCalculationLib.getTierOdds(3, 5, _grandPrizePeriodDraws);
177 | TIER_ODDS_1_8 = TierCalculationLib.getTierOdds(1, 6, _grandPrizePeriodDraws);
178 | TIER_ODDS_2_8 = TierCalculationLib.getTierOdds(2, 6, _grandPrizePeriodDraws);
179 | TIER_ODDS_3_8 = TierCalculationLib.getTierOdds(3, 6, _grandPrizePeriodDraws);
180 | TIER_ODDS_4_8 = TierCalculationLib.getTierOdds(4, 6, _grandPrizePeriodDraws);
181 | TIER_ODDS_1_9 = TierCalculationLib.getTierOdds(1, 7, _grandPrizePeriodDraws);
182 | TIER_ODDS_2_9 = TierCalculationLib.getTierOdds(2, 7, _grandPrizePeriodDraws);
183 | TIER_ODDS_3_9 = TierCalculationLib.getTierOdds(3, 7, _grandPrizePeriodDraws);
184 | TIER_ODDS_4_9 = TierCalculationLib.getTierOdds(4, 7, _grandPrizePeriodDraws);
185 | TIER_ODDS_5_9 = TierCalculationLib.getTierOdds(5, 7, _grandPrizePeriodDraws);
186 | TIER_ODDS_1_10 = TierCalculationLib.getTierOdds(1, 8, _grandPrizePeriodDraws);
187 | TIER_ODDS_2_10 = TierCalculationLib.getTierOdds(2, 8, _grandPrizePeriodDraws);
188 | TIER_ODDS_3_10 = TierCalculationLib.getTierOdds(3, 8, _grandPrizePeriodDraws);
189 | TIER_ODDS_4_10 = TierCalculationLib.getTierOdds(4, 8, _grandPrizePeriodDraws);
190 | TIER_ODDS_5_10 = TierCalculationLib.getTierOdds(5, 8, _grandPrizePeriodDraws);
191 | TIER_ODDS_6_10 = TierCalculationLib.getTierOdds(6, 8, _grandPrizePeriodDraws);
192 | TIER_ODDS_1_11 = TierCalculationLib.getTierOdds(1, 9, _grandPrizePeriodDraws);
193 | TIER_ODDS_2_11 = TierCalculationLib.getTierOdds(2, 9, _grandPrizePeriodDraws);
194 | TIER_ODDS_3_11 = TierCalculationLib.getTierOdds(3, 9, _grandPrizePeriodDraws);
195 | TIER_ODDS_4_11 = TierCalculationLib.getTierOdds(4, 9, _grandPrizePeriodDraws);
196 | TIER_ODDS_5_11 = TierCalculationLib.getTierOdds(5, 9, _grandPrizePeriodDraws);
197 | TIER_ODDS_6_11 = TierCalculationLib.getTierOdds(6, 9, _grandPrizePeriodDraws);
198 | TIER_ODDS_7_11 = TierCalculationLib.getTierOdds(7, 9, _grandPrizePeriodDraws);
199 |
200 | ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS = _sumTierPrizeCounts(4);
201 | ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS = _sumTierPrizeCounts(5);
202 | ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS = _sumTierPrizeCounts(6);
203 | ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS = _sumTierPrizeCounts(7);
204 | ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS = _sumTierPrizeCounts(8);
205 | ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS = _sumTierPrizeCounts(9);
206 | ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS = _sumTierPrizeCounts(10);
207 | ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS = _sumTierPrizeCounts(11);
208 | }
209 |
210 | /// @notice Adjusts the number of tiers and distributes new liquidity.
211 | /// @param _awardingDraw The ID of the draw that is being awarded
212 | /// @param _nextNumberOfTiers The new number of tiers. Must be greater than minimum
213 | /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve
214 | function _awardDraw(
215 | uint24 _awardingDraw,
216 | uint8 _nextNumberOfTiers,
217 | uint256 _prizeTokenLiquidity
218 | ) internal {
219 | if (_nextNumberOfTiers < MINIMUM_NUMBER_OF_TIERS) {
220 | revert NumberOfTiersLessThanMinimum(_nextNumberOfTiers);
221 | }
222 |
223 | uint8 numTiers = numberOfTiers;
224 | uint128 _prizeTokenPerShare = prizeTokenPerShare;
225 | (uint96 deltaReserve, uint128 newPrizeTokenPerShare) = _computeNewDistributions(
226 | numTiers,
227 | _nextNumberOfTiers,
228 | _prizeTokenPerShare,
229 | _prizeTokenLiquidity
230 | );
231 |
232 | uint8 start = _computeReclamationStart(numTiers, _nextNumberOfTiers);
233 | uint8 end = _nextNumberOfTiers;
234 | for (uint8 i = start; i < end; i++) {
235 | _tiers[i] = Tier({
236 | drawId: _awardingDraw,
237 | prizeTokenPerShare: _prizeTokenPerShare,
238 | prizeSize: _computePrizeSize(
239 | i,
240 | _nextNumberOfTiers,
241 | _prizeTokenPerShare,
242 | newPrizeTokenPerShare
243 | )
244 | });
245 | }
246 |
247 | prizeTokenPerShare = newPrizeTokenPerShare;
248 | numberOfTiers = _nextNumberOfTiers;
249 | _lastAwardedDrawId = _awardingDraw;
250 | lastAwardedDrawAwardedAt = uint48(block.timestamp);
251 | _reserve += deltaReserve;
252 | }
253 |
254 | /// @notice Computes the liquidity that will be distributed for the next awarded draw given the next number of tiers and prize liquidity.
255 | /// @param _numberOfTiers The current number of tiers
256 | /// @param _nextNumberOfTiers The next number of tiers to use to compute distribution
257 | /// @param _currentPrizeTokenPerShare The current prize token per share
258 | /// @param _prizeTokenLiquidity The amount of fresh liquidity to distribute across the tiers and reserve
259 | /// @return deltaReserve The amount of liquidity that will be added to the reserve
260 | /// @return newPrizeTokenPerShare The new prize token per share
261 | function _computeNewDistributions(
262 | uint8 _numberOfTiers,
263 | uint8 _nextNumberOfTiers,
264 | uint128 _currentPrizeTokenPerShare,
265 | uint256 _prizeTokenLiquidity
266 | ) internal view returns (uint96 deltaReserve, uint128 newPrizeTokenPerShare) {
267 | uint256 reclaimedLiquidity;
268 | {
269 | // need to redistribute to the canary tier and any new tiers (if expanding)
270 | uint8 start = _computeReclamationStart(_numberOfTiers, _nextNumberOfTiers);
271 | uint8 end = _numberOfTiers;
272 | for (uint8 i = start; i < end; i++) {
273 | reclaimedLiquidity = reclaimedLiquidity + (
274 | _getTierRemainingLiquidity(
275 | _tiers[i].prizeTokenPerShare,
276 | _currentPrizeTokenPerShare,
277 | _numShares(i, _numberOfTiers)
278 | )
279 | );
280 | }
281 | }
282 |
283 | uint256 totalNewLiquidity = _prizeTokenLiquidity + reclaimedLiquidity;
284 | uint256 nextTotalShares = computeTotalShares(_nextNumberOfTiers);
285 | uint256 deltaPrizeTokensPerShare = totalNewLiquidity / nextTotalShares;
286 |
287 | newPrizeTokenPerShare = SafeCast.toUint128(_currentPrizeTokenPerShare + deltaPrizeTokensPerShare);
288 |
289 | deltaReserve = SafeCast.toUint96(
290 | // reserve portion of new liquidity
291 | deltaPrizeTokensPerShare *
292 | reserveShares +
293 | // remainder left over from shares
294 | totalNewLiquidity -
295 | deltaPrizeTokensPerShare *
296 | nextTotalShares
297 | );
298 | }
299 |
300 | /// @notice Returns the prize size for the given tier.
301 | /// @param _tier The tier to retrieve
302 | /// @return The prize size for the tier
303 | function getTierPrizeSize(uint8 _tier) external view returns (uint104) {
304 | uint8 _numTiers = numberOfTiers;
305 |
306 | return
307 | !TierCalculationLib.isValidTier(_tier, _numTiers) ? 0 : _getTier(_tier, _numTiers).prizeSize;
308 | }
309 |
310 | /// @notice Returns the estimated number of prizes for the given tier.
311 | /// @param _tier The tier to retrieve
312 | /// @return The estimated number of prizes
313 | function getTierPrizeCount(uint8 _tier) external pure returns (uint32) {
314 | return uint32(TierCalculationLib.prizeCount(_tier));
315 | }
316 |
317 | /// @notice Retrieves an up-to-date Tier struct for the given tier.
318 | /// @param _tier The tier to retrieve
319 | /// @param _numberOfTiers The number of tiers, should match the current. Passed explicitly as an optimization
320 | /// @return An up-to-date Tier struct; if the prize is outdated then it is recomputed based on available liquidity and the draw ID is updated.
321 | function _getTier(uint8 _tier, uint8 _numberOfTiers) internal view returns (Tier memory) {
322 | Tier memory tier = _tiers[_tier];
323 | uint24 lastAwardedDrawId_ = _lastAwardedDrawId;
324 | if (tier.drawId != lastAwardedDrawId_) {
325 | tier.drawId = lastAwardedDrawId_;
326 | tier.prizeSize = _computePrizeSize(
327 | _tier,
328 | _numberOfTiers,
329 | tier.prizeTokenPerShare,
330 | prizeTokenPerShare
331 | );
332 | }
333 | return tier;
334 | }
335 |
336 | /// @notice Computes the total shares in the system.
337 | /// @return The total shares
338 | function getTotalShares() external view returns (uint256) {
339 | return computeTotalShares(numberOfTiers);
340 | }
341 |
342 | /// @notice Computes the total shares in the system given the number of tiers.
343 | /// @param _numberOfTiers The number of tiers to calculate the total shares for
344 | /// @return The total shares
345 | function computeTotalShares(uint8 _numberOfTiers) public view returns (uint256) {
346 | return uint256(_numberOfTiers-2) * uint256(tierShares) + uint256(reserveShares) + uint256(canaryShares) * 2;
347 | }
348 |
349 | /// @notice Determines at which tier we need to start reclaiming liquidity.
350 | /// @param _numberOfTiers The current number of tiers
351 | /// @param _nextNumberOfTiers The next number of tiers
352 | /// @return The tier to start reclaiming liquidity from
353 | function _computeReclamationStart(uint8 _numberOfTiers, uint8 _nextNumberOfTiers) internal pure returns (uint8) {
354 | // We must always reset the canary tiers, both old and new.
355 | // If the next num is less than the num tiers, then the first canary tiers to reset are the last of the next tiers.
356 | // Otherwise, the canary tiers to reset are the last of the current tiers.
357 | return (_nextNumberOfTiers > _numberOfTiers ? _numberOfTiers : _nextNumberOfTiers) - NUMBER_OF_CANARY_TIERS;
358 | }
359 |
360 | /// @notice Consumes liquidity from the given tier.
361 | /// @param _tierStruct The tier to consume liquidity from
362 | /// @param _tier The tier number
363 | /// @param _liquidity The amount of liquidity to consume
364 | function _consumeLiquidity(Tier memory _tierStruct, uint8 _tier, uint104 _liquidity) internal {
365 | uint8 _tierShares = _numShares(_tier, numberOfTiers);
366 | uint104 remainingLiquidity = SafeCast.toUint104(
367 | _getTierRemainingLiquidity(
368 | _tierStruct.prizeTokenPerShare,
369 | prizeTokenPerShare,
370 | _tierShares
371 | )
372 | );
373 |
374 | if (_liquidity > remainingLiquidity) {
375 | uint96 excess = SafeCast.toUint96(_liquidity - remainingLiquidity);
376 |
377 | if (excess > _reserve) {
378 | revert InsufficientLiquidity(_liquidity);
379 | }
380 |
381 | unchecked {
382 | _reserve -= excess;
383 | }
384 |
385 | emit ReserveConsumed(excess);
386 | _tierStruct.prizeTokenPerShare = prizeTokenPerShare;
387 | } else {
388 | uint8 _remainder = uint8(_liquidity % _tierShares);
389 | uint8 _roundUpConsumption = _remainder == 0 ? 0 : _tierShares - _remainder;
390 | if (_roundUpConsumption > 0) {
391 | // We must round up our tier prize token per share value to ensure we don't over-award the tier's
392 | // liquidity, but any extra rounded up consumption can be contributed to the reserve so every wei
393 | // is accounted for.
394 | _reserve += _roundUpConsumption;
395 | }
396 |
397 | // We know that the rounded up `liquidity` won't exceed the `remainingLiquidity` since the `remainingLiquidity`
398 | // is an integer multiple of `_tierShares` and we check above that `_liquidity <= remainingLiquidity`.
399 | _tierStruct.prizeTokenPerShare += SafeCast.toUint104(uint256(_liquidity) + _roundUpConsumption) / _tierShares;
400 | }
401 |
402 | _tiers[_tier] = _tierStruct;
403 | }
404 |
405 | /// @notice Computes the prize size of the given tier.
406 | /// @param _tier The tier to compute the prize size of
407 | /// @param _numberOfTiers The current number of tiers
408 | /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct
409 | /// @param _prizeTokenPerShare The global prizeTokenPerShare
410 | /// @return The prize size
411 | function _computePrizeSize(
412 | uint8 _tier,
413 | uint8 _numberOfTiers,
414 | uint128 _tierPrizeTokenPerShare,
415 | uint128 _prizeTokenPerShare
416 | ) internal view returns (uint104) {
417 | uint256 prizeCount = TierCalculationLib.prizeCount(_tier);
418 | uint256 remainingTierLiquidity = _getTierRemainingLiquidity(
419 | _tierPrizeTokenPerShare,
420 | _prizeTokenPerShare,
421 | _numShares(_tier, _numberOfTiers)
422 | );
423 |
424 | uint256 prizeSize = convert(
425 | convert(remainingTierLiquidity).mul(tierLiquidityUtilizationRate).div(convert(prizeCount))
426 | );
427 |
428 | return prizeSize > type(uint104).max ? type(uint104).max : uint104(prizeSize);
429 | }
430 |
431 | /// @notice Returns whether the given tier is a canary tier
432 | /// @param _tier The tier to check
433 | /// @return True if the passed tier is a canary tier, false otherwise
434 | function isCanaryTier(uint8 _tier) public view returns (bool) {
435 | return _tier >= numberOfTiers - NUMBER_OF_CANARY_TIERS;
436 | }
437 |
438 | /// @notice Returns the number of shares for the given tier and number of tiers.
439 | /// @param _tier The tier to compute the number of shares for
440 | /// @param _numberOfTiers The number of tiers
441 | /// @return The number of shares
442 | function _numShares(uint8 _tier, uint8 _numberOfTiers) internal view returns (uint8) {
443 | uint8 result = _tier > _numberOfTiers - 3 ? canaryShares : tierShares;
444 | return result;
445 | }
446 |
447 | /// @notice Computes the remaining liquidity available to a tier.
448 | /// @param _tier The tier to compute the liquidity for
449 | /// @return The remaining liquidity
450 | function getTierRemainingLiquidity(uint8 _tier) public view returns (uint256) {
451 | uint8 _numTiers = numberOfTiers;
452 | if (TierCalculationLib.isValidTier(_tier, _numTiers)) {
453 | return _getTierRemainingLiquidity(
454 | _getTier(_tier, _numTiers).prizeTokenPerShare,
455 | prizeTokenPerShare,
456 | _numShares(_tier, _numTiers)
457 | );
458 | } else {
459 | return 0;
460 | }
461 | }
462 |
463 | /// @notice Computes the remaining tier liquidity.
464 | /// @param _tierPrizeTokenPerShare The prizeTokenPerShare of the Tier struct
465 | /// @param _prizeTokenPerShare The global prizeTokenPerShare
466 | /// @param _tierShares The number of shares for the tier
467 | /// @return The remaining available liquidity
468 | function _getTierRemainingLiquidity(
469 | uint128 _tierPrizeTokenPerShare,
470 | uint128 _prizeTokenPerShare,
471 | uint8 _tierShares
472 | ) internal pure returns (uint256) {
473 | uint256 result =
474 | _tierPrizeTokenPerShare >= _prizeTokenPerShare
475 | ? 0
476 | : uint256(_prizeTokenPerShare - _tierPrizeTokenPerShare) * _tierShares;
477 | return result;
478 | }
479 |
480 | /// @notice Estimates the number of prizes for the current number of tiers, including the first canary tier
481 | /// @return The estimated number of prizes including the canary tier
482 | function estimatedPrizeCount() external view returns (uint32) {
483 | return estimatedPrizeCount(numberOfTiers);
484 | }
485 |
486 | /// @notice Estimates the number of prizes for the current number of tiers, including both canary tiers
487 | /// @return The estimated number of prizes including both canary tiers
488 | function estimatedPrizeCountWithBothCanaries() external view returns (uint32) {
489 | return estimatedPrizeCountWithBothCanaries(numberOfTiers);
490 | }
491 |
492 | /// @notice Returns the balance of the reserve.
493 | /// @return The amount of tokens that have been reserved.
494 | function reserve() external view returns (uint96) {
495 | return _reserve;
496 | }
497 |
498 | /// @notice Estimates the prize count for the given number of tiers, including the first canary tier. It expects no prizes are claimed for the last canary tier
499 | /// @param numTiers The number of prize tiers
500 | /// @return The estimated total number of prizes
501 | function estimatedPrizeCount(
502 | uint8 numTiers
503 | ) public view returns (uint32) {
504 | if (numTiers == 4) {
505 | return ESTIMATED_PRIZES_PER_DRAW_FOR_4_TIERS;
506 | } else if (numTiers == 5) {
507 | return ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS;
508 | } else if (numTiers == 6) {
509 | return ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS;
510 | } else if (numTiers == 7) {
511 | return ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS;
512 | } else if (numTiers == 8) {
513 | return ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS;
514 | } else if (numTiers == 9) {
515 | return ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS;
516 | } else if (numTiers == 10) {
517 | return ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS;
518 | } else if (numTiers == 11) {
519 | return ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS;
520 | }
521 | return 0;
522 | }
523 |
524 | /// @notice Estimates the prize count for the given tier, including BOTH canary tiers
525 | /// @param numTiers The number of tiers
526 | /// @return The estimated prize count across all tiers, including both canary tiers.
527 | function estimatedPrizeCountWithBothCanaries(
528 | uint8 numTiers
529 | ) public view returns (uint32) {
530 | if (numTiers >= MINIMUM_NUMBER_OF_TIERS && numTiers <= MAXIMUM_NUMBER_OF_TIERS) {
531 | return estimatedPrizeCount(numTiers) + uint32(TierCalculationLib.prizeCount(numTiers - 1));
532 | } else {
533 | return 0;
534 | }
535 | }
536 |
537 | /// @notice Estimates the number of tiers for the given prize count.
538 | /// @param _prizeCount The number of prizes that were claimed
539 | /// @return The estimated tier
540 | function _estimateNumberOfTiersUsingPrizeCountPerDraw(
541 | uint32 _prizeCount
542 | ) internal view returns (uint8) {
543 | // the prize count is slightly more than 4x for each higher tier. i.e. 16, 66, 270, 1108, etc
544 | // by doubling the measured count, we create a safe margin for error.
545 | uint32 _adjustedPrizeCount = _prizeCount * 2;
546 | if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_5_TIERS) {
547 | return 4;
548 | } else if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_6_TIERS) {
549 | return 5;
550 | } else if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_7_TIERS) {
551 | return 6;
552 | } else if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_8_TIERS) {
553 | return 7;
554 | } else if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_9_TIERS) {
555 | return 8;
556 | } else if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_10_TIERS) {
557 | return 9;
558 | } else if (_adjustedPrizeCount < ESTIMATED_PRIZES_PER_DRAW_FOR_11_TIERS) {
559 | return 10;
560 | } else {
561 | return 11;
562 | }
563 | }
564 |
565 | /// @notice Computes the expected number of prizes for a given number of tiers.
566 | /// @dev Includes the first canary tier prizes, but not the second since the first is expected to
567 | /// be claimed.
568 | /// @param _numTiers The number of tiers, including canaries
569 | /// @return The expected number of prizes, first canary included.
570 | function _sumTierPrizeCounts(uint8 _numTiers) internal view returns (uint32) {
571 | uint32 prizeCount;
572 | uint8 i = 0;
573 | do {
574 | prizeCount += TierCalculationLib.tierPrizeCountPerDraw(i, getTierOdds(i, _numTiers));
575 | i++;
576 | } while (i < _numTiers - 1);
577 | return prizeCount;
578 | }
579 |
580 | /// @notice Computes the odds for a tier given the number of tiers.
581 | /// @param _tier The tier to compute odds for
582 | /// @param _numTiers The number of prize tiers
583 | /// @return The odds of the tier
584 | function getTierOdds(uint8 _tier, uint8 _numTiers) public view returns (SD59x18) {
585 | if (_tier == 0) return TIER_ODDS_0;
586 | if (_numTiers == 3) {
587 | if (_tier <= 2) return TIER_ODDS_EVERY_DRAW;
588 | } else if (_numTiers == 4) {
589 | if (_tier <= 3) return TIER_ODDS_EVERY_DRAW;
590 | } else if (_numTiers == 5) {
591 | if (_tier == 1) return TIER_ODDS_1_5;
592 | else if (_tier <= 4) return TIER_ODDS_EVERY_DRAW;
593 | } else if (_numTiers == 6) {
594 | if (_tier == 1) return TIER_ODDS_1_6;
595 | else if (_tier == 2) return TIER_ODDS_2_6;
596 | else if (_tier <= 5) return TIER_ODDS_EVERY_DRAW;
597 | } else if (_numTiers == 7) {
598 | if (_tier == 1) return TIER_ODDS_1_7;
599 | else if (_tier == 2) return TIER_ODDS_2_7;
600 | else if (_tier == 3) return TIER_ODDS_3_7;
601 | else if (_tier <= 6) return TIER_ODDS_EVERY_DRAW;
602 | } else if (_numTiers == 8) {
603 | if (_tier == 1) return TIER_ODDS_1_8;
604 | else if (_tier == 2) return TIER_ODDS_2_8;
605 | else if (_tier == 3) return TIER_ODDS_3_8;
606 | else if (_tier == 4) return TIER_ODDS_4_8;
607 | else if (_tier <= 7) return TIER_ODDS_EVERY_DRAW;
608 | } else if (_numTiers == 9) {
609 | if (_tier == 1) return TIER_ODDS_1_9;
610 | else if (_tier == 2) return TIER_ODDS_2_9;
611 | else if (_tier == 3) return TIER_ODDS_3_9;
612 | else if (_tier == 4) return TIER_ODDS_4_9;
613 | else if (_tier == 5) return TIER_ODDS_5_9;
614 | else if (_tier <= 8) return TIER_ODDS_EVERY_DRAW;
615 | } else if (_numTiers == 10) {
616 | if (_tier == 1) return TIER_ODDS_1_10;
617 | else if (_tier == 2) return TIER_ODDS_2_10;
618 | else if (_tier == 3) return TIER_ODDS_3_10;
619 | else if (_tier == 4) return TIER_ODDS_4_10;
620 | else if (_tier == 5) return TIER_ODDS_5_10;
621 | else if (_tier == 6) return TIER_ODDS_6_10;
622 | else if (_tier <= 9) return TIER_ODDS_EVERY_DRAW;
623 | } else if (_numTiers == 11) {
624 | if (_tier == 1) return TIER_ODDS_1_11;
625 | else if (_tier == 2) return TIER_ODDS_2_11;
626 | else if (_tier == 3) return TIER_ODDS_3_11;
627 | else if (_tier == 4) return TIER_ODDS_4_11;
628 | else if (_tier == 5) return TIER_ODDS_5_11;
629 | else if (_tier == 6) return TIER_ODDS_6_11;
630 | else if (_tier == 7) return TIER_ODDS_7_11;
631 | else if (_tier <= 10) return TIER_ODDS_EVERY_DRAW;
632 | }
633 | return sd(0);
634 | }
635 | }
636 |
--------------------------------------------------------------------------------
/src/PrizePool.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.24;
3 |
4 | import { SafeCast } from "openzeppelin/utils/math/SafeCast.sol";
5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol";
6 | import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
7 | import { SD59x18, convert, sd } from "prb-math/SD59x18.sol";
8 | import { UD60x18, convert } from "prb-math/UD60x18.sol";
9 | import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
10 |
11 | import { DrawAccumulatorLib, Observation, MAX_OBSERVATION_CARDINALITY } from "./libraries/DrawAccumulatorLib.sol";
12 | import { TieredLiquidityDistributor, Tier } from "./abstract/TieredLiquidityDistributor.sol";
13 | import { TierCalculationLib } from "./libraries/TierCalculationLib.sol";
14 |
15 | /* ============ Constants ============ */
16 |
17 | // The minimum draw timeout. A timeout of two is necessary to allow for enough time to close and award a draw.
18 | uint24 constant MINIMUM_DRAW_TIMEOUT = 2;
19 |
20 | /* ============ Errors ============ */
21 |
22 | /// @notice Thrown when the prize pool is constructed with a first draw open timestamp that is in the past
23 | error FirstDrawOpensInPast();
24 |
25 | /// @notice Thrown when the Twab Controller has an incompatible period length
26 | error IncompatibleTwabPeriodLength();
27 |
28 | /// @notice Thrown when the Twab Controller has an incompatible period offset
29 | error IncompatibleTwabPeriodOffset();
30 |
31 | /// @notice Thrown when someone tries to set the draw manager with the zero address
32 | error DrawManagerIsZeroAddress();
33 |
34 | /// @notice Thrown when the passed creator is the zero address
35 | error CreatorIsZeroAddress();
36 |
37 | /// @notice Thrown when the caller is not the deployer.
38 | error NotDeployer();
39 |
40 | /// @notice Thrown when the range start draw id is computed with range of zero
41 | error RangeSizeZero();
42 |
43 | /// @notice Thrown if the prize pool has shutdown
44 | error PrizePoolShutdown();
45 |
46 | /// @notice Thrown if the prize pool is not shutdown
47 | error PrizePoolNotShutdown();
48 |
49 | /// @notice Thrown when someone tries to withdraw too many rewards.
50 | /// @param requested The requested reward amount to withdraw
51 | /// @param available The total reward amount available for the caller to withdraw
52 | error InsufficientRewardsError(uint256 requested, uint256 available);
53 |
54 | /// @notice Thrown when an address did not win the specified prize on a vault when claiming.
55 | /// @param vault The vault address
56 | /// @param winner The address checked for the prize
57 | /// @param tier The prize tier
58 | /// @param prizeIndex The prize index
59 | error DidNotWin(address vault, address winner, uint8 tier, uint32 prizeIndex);
60 |
61 | /// @notice Thrown when the prize being claimed has already been claimed
62 | /// @param vault The vault address
63 | /// @param winner The address checked for the prize
64 | /// @param tier The prize tier
65 | /// @param prizeIndex The prize index
66 | error AlreadyClaimed(address vault, address winner, uint8 tier, uint32 prizeIndex);
67 |
68 | /// @notice Thrown when the claim reward exceeds the maximum.
69 | /// @param reward The reward being claimed
70 | /// @param maxReward The max reward that can be claimed
71 | error RewardTooLarge(uint256 reward, uint256 maxReward);
72 |
73 | /// @notice Thrown when the contributed amount is more than the available, un-accounted balance.
74 | /// @param amount The contribution amount that is being claimed
75 | /// @param available The available un-accounted balance that can be claimed as a contribution
76 | error ContributionGTDeltaBalance(uint256 amount, uint256 available);
77 |
78 | /// @notice Thrown when the withdraw amount is greater than the available reserve.
79 | /// @param amount The amount being withdrawn
80 | /// @param reserve The total reserve available for withdrawal
81 | error InsufficientReserve(uint104 amount, uint104 reserve);
82 |
83 | /// @notice Thrown when the winning random number is zero.
84 | error RandomNumberIsZero();
85 |
86 | /// @notice Thrown when the draw cannot be awarded since it has not closed.
87 | /// @param drawClosesAt The timestamp in seconds at which the draw closes
88 | error AwardingDrawNotClosed(uint48 drawClosesAt);
89 |
90 | /// @notice Thrown when prize index is greater or equal to the max prize count for the tier.
91 | /// @param invalidPrizeIndex The invalid prize index
92 | /// @param prizeCount The prize count for the tier
93 | /// @param tier The tier number
94 | error InvalidPrizeIndex(uint32 invalidPrizeIndex, uint32 prizeCount, uint8 tier);
95 |
96 | /// @notice Thrown when there are no awarded draws when a computation requires an awarded draw.
97 | error NoDrawsAwarded();
98 |
99 | /// @notice Thrown when the prize pool is initialized with a draw timeout lower than the minimum.
100 | /// @param drawTimeout The draw timeout that was set
101 | /// @param minimumDrawTimeout The minimum draw timeout
102 | error DrawTimeoutLtMinimum(uint24 drawTimeout, uint24 minimumDrawTimeout);
103 |
104 | /// @notice Thrown when the Prize Pool is constructed with a draw timeout greater than the grand prize period draws
105 | error DrawTimeoutGTGrandPrizePeriodDraws();
106 |
107 | /// @notice Thrown when attempting to claim from a tier that does not exist.
108 | /// @param tier The tier number that does not exist
109 | /// @param numberOfTiers The current number of tiers
110 | error InvalidTier(uint8 tier, uint8 numberOfTiers);
111 |
112 | /// @notice Thrown when the caller is not the draw manager.
113 | /// @param caller The caller address
114 | /// @param drawManager The drawManager address
115 | error CallerNotDrawManager(address caller, address drawManager);
116 |
117 | /// @notice Thrown when someone tries to claim a prize that is zero size
118 | error PrizeIsZero();
119 |
120 | /// @notice Thrown when someone tries to claim a prize, but sets the reward recipient address to the zero address.
121 | error RewardRecipientZeroAddress();
122 |
123 | /// @notice Thrown when a claim is attempted after the claiming period has expired.
124 | error ClaimPeriodExpired();
125 |
126 | /// @notice Thrown when anyone but the creator calls a privileged function
127 | error OnlyCreator();
128 |
129 | /// @notice Thrown when the draw manager has already been set
130 | error DrawManagerAlreadySet();
131 |
132 | /// @notice Thrown when the grand prize period is too large
133 | /// @param grandPrizePeriodDraws The set grand prize period
134 | /// @param maxGrandPrizePeriodDraws The max grand prize period
135 | error GrandPrizePeriodDrawsTooLarge(uint24 grandPrizePeriodDraws, uint24 maxGrandPrizePeriodDraws);
136 |
137 | /// @notice Constructor Parameters
138 | /// @param prizeToken The token to use for prizes
139 | /// @param twabController The Twab Controller to retrieve time-weighted average balances from
140 | /// @param creator The address that will be permitted to finish prize pool initialization after deployment
141 | /// @param tierLiquidityUtilizationRate The rate at which liquidity is utilized for prize tiers. This allows
142 | /// for deviations in prize claims; if 0.75e18 then it is 75% utilization so it can accommodate 25% deviation
143 | /// in more prize claims.
144 | /// @param drawPeriodSeconds The number of seconds between draws.
145 | /// E.g. a Prize Pool with a daily draw should have a draw period of 86400 seconds.
146 | /// @param firstDrawOpensAt The timestamp at which the first draw will open
147 | /// @param grandPrizePeriodDraws The target number of draws to pass between each grand prize
148 | /// @param numberOfTiers The number of tiers to start with. Must be greater than or equal to the minimum
149 | /// number of tiers
150 | /// @param tierShares The number of shares to allocate to each tier
151 | /// @param canaryShares The number of shares to allocate to each canary tier
152 | /// @param reserveShares The number of shares to allocate to the reserve
153 | /// @param drawTimeout The number of draws that need to be missed before the prize pool shuts down. The timeout
154 | /// resets when a draw is awarded.
155 | struct ConstructorParams {
156 | IERC20 prizeToken;
157 | TwabController twabController;
158 | address creator;
159 | uint256 tierLiquidityUtilizationRate;
160 | uint48 drawPeriodSeconds;
161 | uint48 firstDrawOpensAt;
162 | uint24 grandPrizePeriodDraws;
163 | uint8 numberOfTiers;
164 | uint8 tierShares;
165 | uint8 canaryShares;
166 | uint8 reserveShares;
167 | uint24 drawTimeout;
168 | }
169 |
170 | /// @title PoolTogether V5 Prize Pool
171 | /// @author G9 Software Inc. & PoolTogether Inc. Team
172 | /// @notice The Prize Pool holds the prize liquidity and allows vaults to claim prizes.
173 | contract PrizePool is TieredLiquidityDistributor {
174 | using SafeERC20 for IERC20;
175 | using DrawAccumulatorLib for DrawAccumulatorLib.Accumulator;
176 |
177 | /* ============ Events ============ */
178 |
179 | /// @notice Emitted when a prize is claimed.
180 | /// @param vault The address of the vault that claimed the prize.
181 | /// @param winner The address of the winner
182 | /// @param recipient The address of the prize recipient
183 | /// @param drawId The draw ID of the draw that was claimed.
184 | /// @param tier The prize tier that was claimed.
185 | /// @param prizeIndex The index of the prize that was claimed
186 | /// @param payout The amount of prize tokens that were paid out to the winner
187 | /// @param claimReward The amount of prize tokens that were paid to the claimer
188 | /// @param claimRewardRecipient The address that the claimReward was sent to
189 | event ClaimedPrize(
190 | address indexed vault,
191 | address indexed winner,
192 | address indexed recipient,
193 | uint24 drawId,
194 | uint8 tier,
195 | uint32 prizeIndex,
196 | uint152 payout,
197 | uint96 claimReward,
198 | address claimRewardRecipient
199 | );
200 |
201 | /// @notice Emitted when a draw is awarded.
202 | /// @param drawId The ID of the draw that was awarded
203 | /// @param winningRandomNumber The winning random number for the awarded draw
204 | /// @param lastNumTiers The previous number of prize tiers
205 | /// @param numTiers The number of prize tiers for the awarded draw
206 | /// @param reserve The resulting reserve available
207 | /// @param prizeTokensPerShare The amount of prize tokens per share for the awarded draw
208 | /// @param drawOpenedAt The start timestamp of the awarded draw
209 | event DrawAwarded(
210 | uint24 indexed drawId,
211 | uint256 winningRandomNumber,
212 | uint8 lastNumTiers,
213 | uint8 numTiers,
214 | uint104 reserve,
215 | uint128 prizeTokensPerShare,
216 | uint48 drawOpenedAt
217 | );
218 |
219 | /// @notice Emitted when any amount of the reserve is rewarded to a recipient.
220 | /// @param to The recipient of the reward
221 | /// @param amount The amount of assets rewarded
222 | event AllocateRewardFromReserve(address indexed to, uint256 amount);
223 |
224 | /// @notice Emitted when the reserve is manually increased.
225 | /// @param user The user who increased the reserve
226 | /// @param amount The amount of assets transferred
227 | event ContributedReserve(address indexed user, uint256 amount);
228 |
229 | /// @notice Emitted when a vault contributes prize tokens to the pool.
230 | /// @param vault The address of the vault that is contributing tokens
231 | /// @param drawId The ID of the first draw that the tokens will be contributed to
232 | /// @param amount The amount of tokens contributed
233 | event ContributePrizeTokens(address indexed vault, uint24 indexed drawId, uint256 amount);
234 |
235 | /// @notice Emitted when the draw manager is set
236 | /// @param drawManager The address of the draw manager
237 | event SetDrawManager(address indexed drawManager);
238 |
239 | /// @notice Emitted when an address withdraws their prize claim rewards.
240 | /// @param account The account that is withdrawing rewards
241 | /// @param to The address the rewards are sent to
242 | /// @param amount The amount withdrawn
243 | /// @param available The total amount that was available to withdraw before the transfer
244 | event WithdrawRewards(
245 | address indexed account,
246 | address indexed to,
247 | uint256 amount,
248 | uint256 available
249 | );
250 |
251 | /// @notice Emitted when an address receives new prize claim rewards.
252 | /// @param to The address the rewards are given to
253 | /// @param amount The amount increased
254 | event IncreaseClaimRewards(address indexed to, uint256 amount);
255 |
256 | /* ============ State ============ */
257 |
258 | /// @notice The DrawAccumulator that tracks the exponential moving average of the contributions by a vault.
259 | mapping(address vault => DrawAccumulatorLib.Accumulator accumulator) internal _vaultAccumulator;
260 |
261 | /// @notice Records the claim record for a winner.
262 | mapping(address vault => mapping(address account => mapping(uint24 drawId => mapping(uint8 tier => mapping(uint32 prizeIndex => bool claimed)))))
263 | internal _claimedPrizes;
264 |
265 | /// @notice Tracks the total rewards accrued for a claimer or draw completer.
266 | mapping(address recipient => uint256 rewards) internal _rewards;
267 |
268 | /// @notice The special value for the donator address. Contributions from this address are excluded from the total odds.
269 | /// @dev 0x000...F2EE because it's free money!
270 | address public constant DONATOR = 0x000000000000000000000000000000000000F2EE;
271 |
272 | /// @notice The token that is being contributed and awarded as prizes.
273 | IERC20 public immutable prizeToken;
274 |
275 | /// @notice The Twab Controller to use to retrieve historic balances.
276 | TwabController public immutable twabController;
277 |
278 | /// @notice The number of seconds between draws.
279 | uint48 public immutable drawPeriodSeconds;
280 |
281 | /// @notice The timestamp at which the first draw will open.
282 | uint48 public immutable firstDrawOpensAt;
283 |
284 | /// @notice The maximum number of draws that can pass since the last awarded draw before the prize pool is considered inactive.
285 | uint24 public immutable drawTimeout;
286 |
287 | /// @notice The address that is allowed to set the draw manager
288 | address immutable creator;
289 |
290 | /// @notice The exponential weighted average of all vault contributions.
291 | DrawAccumulatorLib.Accumulator internal _totalAccumulator;
292 |
293 | /// @notice The winner random number for the last awarded draw.
294 | uint256 internal _winningRandomNumber;
295 |
296 | /// @notice The draw manager address.
297 | address public drawManager;
298 |
299 | /// @notice Tracks reserve that was contributed directly to the reserve. Always increases.
300 | uint96 internal _directlyContributedReserve;
301 |
302 | /// @notice The number of prize claims for the last awarded draw.
303 | uint24 public claimCount;
304 |
305 | /// @notice The total amount of prize tokens that have been claimed for all time.
306 | uint128 internal _totalWithdrawn;
307 |
308 | /// @notice The total amount of rewards that have yet to be claimed
309 | uint104 internal _totalRewardsToBeClaimed;
310 |
311 | /// @notice The observation at which the shutdown balance was recorded
312 | Observation shutdownObservation;
313 |
314 | /// @notice The balance available to be withdrawn at shutdown
315 | uint256 shutdownBalance;
316 |
317 | /// @notice The total contributed observation that was used for the last withdrawal for a vault and account
318 | mapping(address vault => mapping(address account => Observation lastWithdrawalTotalContributedObservation)) internal _withdrawalObservations;
319 |
320 | /// @notice The shutdown portion of liquidity for a vault and account
321 | mapping(address vault => mapping(address account => UD60x18 shutdownPortion)) internal _shutdownPortions;
322 |
323 | /* ============ Constructor ============ */
324 |
325 | /// @notice Constructs a new Prize Pool.
326 | /// @param params A struct of constructor parameters
327 | constructor(
328 | ConstructorParams memory params
329 | )
330 | TieredLiquidityDistributor(
331 | params.tierLiquidityUtilizationRate,
332 | params.numberOfTiers,
333 | params.tierShares,
334 | params.canaryShares,
335 | params.reserveShares,
336 | params.grandPrizePeriodDraws
337 | )
338 | {
339 | if (params.drawTimeout < MINIMUM_DRAW_TIMEOUT) {
340 | revert DrawTimeoutLtMinimum(params.drawTimeout, MINIMUM_DRAW_TIMEOUT);
341 | }
342 |
343 | if (params.drawTimeout > params.grandPrizePeriodDraws) {
344 | revert DrawTimeoutGTGrandPrizePeriodDraws();
345 | }
346 |
347 | if (params.firstDrawOpensAt < block.timestamp) {
348 | revert FirstDrawOpensInPast();
349 | }
350 |
351 | if (params.grandPrizePeriodDraws >= MAX_OBSERVATION_CARDINALITY) {
352 | revert GrandPrizePeriodDrawsTooLarge(params.grandPrizePeriodDraws, MAX_OBSERVATION_CARDINALITY - 1);
353 | }
354 |
355 | uint48 twabPeriodOffset = params.twabController.PERIOD_OFFSET();
356 | uint48 twabPeriodLength = params.twabController.PERIOD_LENGTH();
357 |
358 | if (
359 | params.drawPeriodSeconds < twabPeriodLength ||
360 | params.drawPeriodSeconds % twabPeriodLength != 0
361 | ) {
362 | revert IncompatibleTwabPeriodLength();
363 | }
364 |
365 | if ((params.firstDrawOpensAt - twabPeriodOffset) % twabPeriodLength != 0) {
366 | revert IncompatibleTwabPeriodOffset();
367 | }
368 |
369 | if (params.creator == address(0)) {
370 | revert CreatorIsZeroAddress();
371 | }
372 |
373 | creator = params.creator;
374 | drawTimeout = params.drawTimeout;
375 | prizeToken = params.prizeToken;
376 | twabController = params.twabController;
377 | drawPeriodSeconds = params.drawPeriodSeconds;
378 | firstDrawOpensAt = params.firstDrawOpensAt;
379 | }
380 |
381 | /* ============ Modifiers ============ */
382 |
383 | /// @notice Modifier that throws if sender is not the draw manager.
384 | modifier onlyDrawManager() {
385 | if (msg.sender != drawManager) {
386 | revert CallerNotDrawManager(msg.sender, drawManager);
387 | }
388 | _;
389 | }
390 |
391 | /// @notice Sets the Draw Manager contract on the prize pool. Can only be called once by the creator.
392 | /// @param _drawManager The address of the Draw Manager contract
393 | function setDrawManager(address _drawManager) external {
394 | if (msg.sender != creator) {
395 | revert OnlyCreator();
396 | }
397 | if (drawManager != address(0)) {
398 | revert DrawManagerAlreadySet();
399 | }
400 | drawManager = _drawManager;
401 |
402 | emit SetDrawManager(_drawManager);
403 | }
404 |
405 | /* ============ External Write Functions ============ */
406 |
407 | /// @notice Contributes prize tokens on behalf of the given vault.
408 | /// @dev The tokens should have already been transferred to the prize pool.
409 | /// @dev The prize pool balance will be checked to ensure there is at least the given amount to deposit.
410 | /// @param _prizeVault The address of the vault to contribute to
411 | /// @param _amount The amount of prize tokens to contribute
412 | /// @return The amount of available prize tokens prior to the contribution.
413 | function contributePrizeTokens(address _prizeVault, uint256 _amount) public returns (uint256) {
414 | uint256 _deltaBalance = prizeToken.balanceOf(address(this)) - accountedBalance();
415 | if (_deltaBalance < _amount) {
416 | revert ContributionGTDeltaBalance(_amount, _deltaBalance);
417 | }
418 | uint24 openDrawId_ = getOpenDrawId();
419 | _vaultAccumulator[_prizeVault].add(_amount, openDrawId_);
420 | _totalAccumulator.add(_amount, openDrawId_);
421 | emit ContributePrizeTokens(_prizeVault, openDrawId_, _amount);
422 | return _deltaBalance;
423 | }
424 |
425 | /// @notice Allows a user to donate prize tokens to the prize pool.
426 | /// @param _amount The amount of tokens to donate. The amount should already be approved for transfer.
427 | function donatePrizeTokens(uint256 _amount) external {
428 | prizeToken.safeTransferFrom(msg.sender, address(this), _amount);
429 | contributePrizeTokens(DONATOR, _amount);
430 | }
431 |
432 | /// @notice Allows the Manager to allocate a reward from the reserve to a recipient.
433 | /// @param _to The address to allocate the rewards to
434 | /// @param _amount The amount of tokens for the reward
435 | function allocateRewardFromReserve(address _to, uint96 _amount) external onlyDrawManager notShutdown {
436 | if (_to == address(0)) {
437 | revert RewardRecipientZeroAddress();
438 | }
439 | if (_amount > _reserve) {
440 | revert InsufficientReserve(_amount, _reserve);
441 | }
442 |
443 | unchecked {
444 | _reserve -= _amount;
445 | }
446 |
447 | _rewards[_to] += _amount;
448 | _totalRewardsToBeClaimed = SafeCast.toUint104(_totalRewardsToBeClaimed + _amount);
449 | emit AllocateRewardFromReserve(_to, _amount);
450 | }
451 |
452 | /// @notice Allows the Manager to award a draw with the winning random number.
453 | /// @dev Updates the number of tiers, the winning random number and the prize pool reserve.
454 | /// @param winningRandomNumber_ The winning random number for the draw
455 | /// @return The ID of the awarded draw
456 | function awardDraw(uint256 winningRandomNumber_) external onlyDrawManager notShutdown returns (uint24) {
457 | // check winning random number
458 | if (winningRandomNumber_ == 0) {
459 | revert RandomNumberIsZero();
460 | }
461 | uint24 awardingDrawId = getDrawIdToAward();
462 | uint48 awardingDrawOpenedAt = drawOpensAt(awardingDrawId);
463 | uint48 awardingDrawClosedAt = awardingDrawOpenedAt + drawPeriodSeconds;
464 | if (block.timestamp < awardingDrawClosedAt) {
465 | revert AwardingDrawNotClosed(awardingDrawClosedAt);
466 | }
467 |
468 | uint24 lastAwardedDrawId_ = _lastAwardedDrawId;
469 | uint32 _claimCount = claimCount;
470 | uint8 _numTiers = numberOfTiers;
471 | uint8 _nextNumberOfTiers = _numTiers;
472 |
473 | _nextNumberOfTiers = computeNextNumberOfTiers(_claimCount);
474 |
475 | // If any draws were skipped from the last awarded draw to the one we are awarding, the contribution
476 | // from those skipped draws will be included in the new distributions.
477 | _awardDraw(
478 | awardingDrawId,
479 | _nextNumberOfTiers,
480 | getTotalContributedBetween(lastAwardedDrawId_ + 1, awardingDrawId)
481 | );
482 |
483 | _winningRandomNumber = winningRandomNumber_;
484 | if (_claimCount != 0) {
485 | claimCount = 0;
486 | }
487 |
488 | emit DrawAwarded(
489 | awardingDrawId,
490 | winningRandomNumber_,
491 | _numTiers,
492 | _nextNumberOfTiers,
493 | _reserve,
494 | prizeTokenPerShare,
495 | awardingDrawOpenedAt
496 | );
497 |
498 | return awardingDrawId;
499 | }
500 |
501 | /// @notice Claims a prize for a given winner and tier.
502 | /// @dev This function takes in an address _winner, a uint8 _tier, a uint96 _claimReward, and an
503 | /// address _claimRewardRecipient. It checks if _winner is actually the winner of the _tier for the calling vault.
504 | /// If so, it calculates the prize size and transfers it to the winner. If not, it reverts with an error message.
505 | /// The function then checks the claim record of _winner to see if they have already claimed the prize for the
506 | /// awarded draw. If not, it updates the claim record with the claimed tier and emits a ClaimedPrize event with
507 | /// information about the claim.
508 | /// Note that this function can modify the state of the contract by updating the claim record, changing the largest
509 | /// tier claimed and the claim count, and transferring prize tokens. The function is marked as external which
510 | /// means that it can be called from outside the contract.
511 | /// @param _winner The address of the eligible winner
512 | /// @param _tier The tier of the prize to be claimed.
513 | /// @param _prizeIndex The prize to claim for the winner. Must be less than the prize count for the tier.
514 | /// @param _prizeRecipient The recipient of the prize
515 | /// @param _claimReward The claimReward associated with claiming the prize.
516 | /// @param _claimRewardRecipient The address to receive the claimReward.
517 | /// @return Total prize amount claimed (payout and claimRewards combined).
518 | function claimPrize(
519 | address _winner,
520 | uint8 _tier,
521 | uint32 _prizeIndex,
522 | address _prizeRecipient,
523 | uint96 _claimReward,
524 | address _claimRewardRecipient
525 | ) external returns (uint256) {
526 | /// @dev Claims cannot occur after a draw has been finalized (1 period after a draw closes). This prevents
527 | /// the reserve from changing while the following draw is being awarded.
528 | uint24 lastAwardedDrawId_ = _lastAwardedDrawId;
529 | if (isDrawFinalized(lastAwardedDrawId_)) {
530 | revert ClaimPeriodExpired();
531 | }
532 | if (_claimRewardRecipient == address(0) && _claimReward > 0) {
533 | revert RewardRecipientZeroAddress();
534 | }
535 |
536 | uint8 _numTiers = numberOfTiers;
537 |
538 | Tier memory tierLiquidity = _getTier(_tier, _numTiers);
539 |
540 | if (_claimReward > tierLiquidity.prizeSize) {
541 | revert RewardTooLarge(_claimReward, tierLiquidity.prizeSize);
542 | }
543 |
544 | if (tierLiquidity.prizeSize == 0) {
545 | revert PrizeIsZero();
546 | }
547 |
548 | if (!isWinner(msg.sender, _winner, _tier, _prizeIndex)) {
549 | revert DidNotWin(msg.sender, _winner, _tier, _prizeIndex);
550 | }
551 |
552 | if (_claimedPrizes[msg.sender][_winner][lastAwardedDrawId_][_tier][_prizeIndex]) {
553 | revert AlreadyClaimed(msg.sender, _winner, _tier, _prizeIndex);
554 | }
555 |
556 | _claimedPrizes[msg.sender][_winner][lastAwardedDrawId_][_tier][_prizeIndex] = true;
557 |
558 | _consumeLiquidity(tierLiquidity, _tier, tierLiquidity.prizeSize);
559 |
560 | // `amount` is the payout amount
561 | uint256 amount;
562 | if (_claimReward != 0) {
563 | emit IncreaseClaimRewards(_claimRewardRecipient, _claimReward);
564 | _rewards[_claimRewardRecipient] += _claimReward;
565 |
566 | unchecked {
567 | amount = tierLiquidity.prizeSize - _claimReward;
568 | }
569 | } else {
570 | amount = tierLiquidity.prizeSize;
571 | }
572 |
573 | // co-locate to save gas
574 | claimCount++;
575 | _totalWithdrawn = SafeCast.toUint128(_totalWithdrawn + amount);
576 | _totalRewardsToBeClaimed = SafeCast.toUint104(_totalRewardsToBeClaimed + _claimReward);
577 |
578 | emit ClaimedPrize(
579 | msg.sender,
580 | _winner,
581 | _prizeRecipient,
582 | lastAwardedDrawId_,
583 | _tier,
584 | _prizeIndex,
585 | uint152(amount),
586 | _claimReward,
587 | _claimRewardRecipient
588 | );
589 |
590 | if (amount > 0) {
591 | prizeToken.safeTransfer(_prizeRecipient, amount);
592 | }
593 |
594 | return tierLiquidity.prizeSize;
595 | }
596 |
597 | /// @notice Withdraws earned rewards for the caller.
598 | /// @param _to The address to transfer the rewards to
599 | /// @param _amount The amount of rewards to withdraw
600 | function withdrawRewards(address _to, uint256 _amount) external {
601 | uint256 _available = _rewards[msg.sender];
602 |
603 | if (_amount > _available) {
604 | revert InsufficientRewardsError(_amount, _available);
605 | }
606 |
607 | unchecked {
608 | _rewards[msg.sender] = _available - _amount;
609 | }
610 |
611 | _totalWithdrawn = SafeCast.toUint128(_totalWithdrawn + _amount);
612 | _totalRewardsToBeClaimed = SafeCast.toUint104(_totalRewardsToBeClaimed - _amount);
613 |
614 | // skip transfer if recipient is the prize pool (tokens stay in this contract)
615 | if (_to != address(this)) {
616 | prizeToken.safeTransfer(_to, _amount);
617 | }
618 |
619 | emit WithdrawRewards(msg.sender, _to, _amount, _available);
620 | }
621 |
622 | /// @notice Allows anyone to deposit directly into the Prize Pool reserve.
623 | /// @dev Ensure caller has sufficient balance and has approved the Prize Pool to transfer the tokens
624 | /// @param _amount The amount of tokens to increase the reserve by
625 | function contributeReserve(uint96 _amount) external notShutdown {
626 | _reserve += _amount;
627 | _directlyContributedReserve += _amount;
628 | prizeToken.safeTransferFrom(msg.sender, address(this), _amount);
629 | emit ContributedReserve(msg.sender, _amount);
630 | }
631 |
632 | /* ============ External Read Functions ============ */
633 |
634 | /// @notice Returns the winning random number for the last awarded draw.
635 | /// @return The winning random number
636 | function getWinningRandomNumber() external view returns (uint256) {
637 | return _winningRandomNumber;
638 | }
639 |
640 | /// @notice Returns the last awarded draw id.
641 | /// @return The last awarded draw id
642 | function getLastAwardedDrawId() external view returns (uint24) {
643 | return _lastAwardedDrawId;
644 | }
645 |
646 | /// @notice Returns the total prize tokens contributed by a particular vault between the given draw ids, inclusive.
647 | /// @param _vault The address of the vault
648 | /// @param _startDrawIdInclusive Start draw id inclusive
649 | /// @param _endDrawIdInclusive End draw id inclusive
650 | /// @return The total prize tokens contributed by the given vault
651 | function getContributedBetween(
652 | address _vault,
653 | uint24 _startDrawIdInclusive,
654 | uint24 _endDrawIdInclusive
655 | ) external view returns (uint256) {
656 | return
657 | _vaultAccumulator[_vault].getDisbursedBetween(
658 | _startDrawIdInclusive,
659 | _endDrawIdInclusive
660 | );
661 | }
662 |
663 | /// @notice Returns the total prize tokens donated to the prize pool
664 | /// @param _startDrawIdInclusive Start draw id inclusive
665 | /// @param _endDrawIdInclusive End draw id inclusive
666 | /// @return The total prize tokens donated to the prize pool
667 | function getDonatedBetween(
668 | uint24 _startDrawIdInclusive,
669 | uint24 _endDrawIdInclusive
670 | ) external view returns (uint256) {
671 | return
672 | _vaultAccumulator[DONATOR].getDisbursedBetween(
673 | _startDrawIdInclusive,
674 | _endDrawIdInclusive
675 | );
676 | }
677 |
678 | /// @notice Returns the newest observation for the total accumulator
679 | /// @return The newest observation
680 | function getTotalAccumulatorNewestObservation() external view returns (Observation memory) {
681 | return _totalAccumulator.newestObservation();
682 | }
683 |
684 | /// @notice Returns the newest observation for the specified vault accumulator
685 | /// @param _vault The vault to check
686 | /// @return The newest observation for the vault
687 | function getVaultAccumulatorNewestObservation(address _vault) external view returns (Observation memory) {
688 | return _vaultAccumulator[_vault].newestObservation();
689 | }
690 |
691 | /// @notice Computes the expected duration prize accrual for a tier.
692 | /// @param _tier The tier to check
693 | /// @return The number of draws
694 | function getTierAccrualDurationInDraws(uint8 _tier) external view returns (uint24) {
695 | return
696 | TierCalculationLib.estimatePrizeFrequencyInDraws(getTierOdds(_tier, numberOfTiers), grandPrizePeriodDraws);
697 | }
698 |
699 | /// @notice The total amount of prize tokens that have been withdrawn as fees or prizes
700 | /// @return The total amount of prize tokens that have been withdrawn as fees or prizes
701 | function totalWithdrawn() external view returns (uint256) {
702 | return _totalWithdrawn;
703 | }
704 |
705 | /// @notice Returns the amount of tokens that will be added to the reserve when next draw to award is awarded.
706 | /// @dev Intended for Draw manager to use after a draw has closed but not yet been awarded.
707 | /// @return The amount of prize tokens that will be added to the reserve
708 | function pendingReserveContributions() external view returns (uint256) {
709 | uint8 _numTiers = numberOfTiers;
710 | uint24 lastAwardedDrawId_ = _lastAwardedDrawId;
711 |
712 | (uint104 newReserve, ) = _computeNewDistributions(
713 | _numTiers,
714 | lastAwardedDrawId_ == 0 ? _numTiers : computeNextNumberOfTiers(claimCount),
715 | prizeTokenPerShare,
716 | getTotalContributedBetween(lastAwardedDrawId_ + 1, getDrawIdToAward())
717 | );
718 |
719 | return newReserve;
720 | }
721 |
722 | /// @notice Returns whether the winner has claimed the tier for the last awarded draw
723 | /// @param _vault The vault to check
724 | /// @param _winner The account to check
725 | /// @param _tier The tier to check
726 | /// @param _prizeIndex The prize index to check
727 | /// @return True if the winner claimed the tier for the last awarded draw, false otherwise.
728 | function wasClaimed(
729 | address _vault,
730 | address _winner,
731 | uint8 _tier,
732 | uint32 _prizeIndex
733 | ) external view returns (bool) {
734 | return _claimedPrizes[_vault][_winner][_lastAwardedDrawId][_tier][_prizeIndex];
735 | }
736 |
737 | /// @notice Returns whether the winner has claimed the tier for the specified draw
738 | /// @param _vault The vault to check
739 | /// @param _winner The account to check
740 | /// @param _drawId The draw ID to check
741 | /// @param _tier The tier to check
742 | /// @param _prizeIndex The prize index to check
743 | /// @return True if the winner claimed the tier for the specified draw, false otherwise.
744 | function wasClaimed(
745 | address _vault,
746 | address _winner,
747 | uint24 _drawId,
748 | uint8 _tier,
749 | uint32 _prizeIndex
750 | ) external view returns (bool) {
751 | return _claimedPrizes[_vault][_winner][_drawId][_tier][_prizeIndex];
752 | }
753 |
754 | /// @notice Returns the balance of rewards earned for the given address.
755 | /// @param _recipient The recipient to retrieve the reward balance for
756 | /// @return The balance of rewards for the given recipient
757 | function rewardBalance(address _recipient) external view returns (uint256) {
758 | return _rewards[_recipient];
759 | }
760 |
761 | /// @notice Computes and returns the next number of tiers based on the current prize claim counts. This number may change throughout the draw
762 | /// @return The next number of tiers
763 | function estimateNextNumberOfTiers() external view returns (uint8) {
764 | return computeNextNumberOfTiers(claimCount);
765 | }
766 |
767 | /* ============ Internal Functions ============ */
768 |
769 | /// @notice Computes how many tokens have been accounted for
770 | /// @return The balance of tokens that have been accounted for
771 | function accountedBalance() public view returns (uint256) {
772 | return _accountedBalance(_totalAccumulator.newestObservation());
773 | }
774 |
775 | /// @notice Returns the balance available at the time of shutdown, less rewards to be claimed.
776 | /// @dev This function will compute and store the current balance if it has not yet been set.
777 | /// @return balance The balance that is available for depositors to withdraw
778 | /// @return observation The observation used to compute the balance
779 | function getShutdownInfo() public returns (uint256 balance, Observation memory observation) {
780 | if (!isShutdown()) {
781 | return (balance, observation);
782 | }
783 | // if not initialized
784 | if (shutdownObservation.disbursed + shutdownObservation.available == 0) {
785 | observation = _totalAccumulator.newestObservation();
786 | shutdownObservation = observation;
787 | balance = _accountedBalance(observation) - _totalRewardsToBeClaimed;
788 | shutdownBalance = balance;
789 | } else {
790 | observation = shutdownObservation;
791 | balance = shutdownBalance;
792 | }
793 | }
794 |
795 | /// @notice Returns the open draw ID based on the current block timestamp.
796 | /// @dev Returns `1` if the first draw hasn't opened yet. This prevents any contributions from
797 | /// going to the inaccessible draw zero.
798 | /// @dev First draw has an ID of `1`. This means that if `_lastAwardedDrawId` is zero,
799 | /// we know that no draws have been awarded yet.
800 | /// @dev Capped at the shutdown draw ID if the prize pool has shutdown.
801 | /// @return The ID of the draw period that the current block is in
802 | function getOpenDrawId() public view returns (uint24) {
803 | uint24 shutdownDrawId = getShutdownDrawId();
804 | uint24 openDrawId = getDrawId(block.timestamp);
805 | return openDrawId > shutdownDrawId ? shutdownDrawId : openDrawId;
806 | }
807 |
808 | /// @notice Returns the open draw id for the given timestamp
809 | /// @param _timestamp The timestamp to get the draw id for
810 | /// @return The ID of the open draw that the timestamp is in
811 | function getDrawId(uint256 _timestamp) public view returns (uint24) {
812 | uint48 _firstDrawOpensAt = firstDrawOpensAt;
813 | return
814 | (_timestamp < _firstDrawOpensAt)
815 | ? 1
816 | : (uint24((_timestamp - _firstDrawOpensAt) / drawPeriodSeconds) + 1);
817 | }
818 |
819 | /// @notice Returns the next draw ID that can be awarded.
820 | /// @dev It's possible for draws to be missed, so the next draw ID to award
821 | /// may be more than one draw ahead of the last awarded draw ID.
822 | /// @return The next draw ID that can be awarded
823 | function getDrawIdToAward() public view returns (uint24) {
824 | uint24 openDrawId_ = getOpenDrawId();
825 | return (openDrawId_ - _lastAwardedDrawId) > 1 ? openDrawId_ - 1 : openDrawId_;
826 | }
827 |
828 | /// @notice Returns the time at which a draw opens / opened at.
829 | /// @param drawId The draw to get the timestamp for
830 | /// @return The start time of the draw in seconds
831 | function drawOpensAt(uint24 drawId) public view returns (uint48) {
832 | return firstDrawOpensAt + (drawId - 1) * drawPeriodSeconds;
833 | }
834 |
835 | /// @notice Returns the time at which a draw closes / closed at.
836 | /// @param drawId The draw to get the timestamp for
837 | /// @return The end time of the draw in seconds
838 | function drawClosesAt(uint24 drawId) public view returns (uint48) {
839 | return firstDrawOpensAt + drawId * drawPeriodSeconds;
840 | }
841 |
842 | /// @notice Checks if the given draw is finalized.
843 | /// @param drawId The draw to check
844 | /// @return True if the draw is finalized, false otherwise
845 | function isDrawFinalized(uint24 drawId) public view returns (bool) {
846 | return block.timestamp >= drawClosesAt(drawId + 1);
847 | }
848 |
849 | /// @notice Calculates the number of tiers given the number of prize claims
850 | /// @dev This function will use the claim count to determine the number of tiers, then add one for the canary tier.
851 | /// @param _claimCount The number of prize claims
852 | /// @return The estimated number of tiers + the canary tier
853 | function computeNextNumberOfTiers(uint32 _claimCount) public view returns (uint8) {
854 | if (_lastAwardedDrawId != 0) {
855 | // claimCount is expected to be the estimated number of claims for the current prize tier.
856 | uint8 nextNumberOfTiers = _estimateNumberOfTiersUsingPrizeCountPerDraw(_claimCount);
857 | // limit change to 1 tier
858 | uint8 _numTiers = numberOfTiers;
859 | if (nextNumberOfTiers > _numTiers) {
860 | nextNumberOfTiers = _numTiers + 1;
861 | } else if (nextNumberOfTiers < _numTiers) {
862 | nextNumberOfTiers = _numTiers - 1;
863 | }
864 | return nextNumberOfTiers;
865 | } else {
866 | return numberOfTiers;
867 | }
868 | }
869 |
870 | /// @notice Returns the given account and vault's portion of the shutdown balance.
871 | /// @param _vault The vault whose contributions are measured
872 | /// @param _account The account whose vault twab is measured
873 | /// @return The portion of the shutdown balance that the account is entitled to.
874 | function computeShutdownPortion(address _vault, address _account) public view returns (UD60x18) {
875 | uint24 drawIdPriorToShutdown = getShutdownDrawId() - 1;
876 | uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(drawIdPriorToShutdown, grandPrizePeriodDraws);
877 |
878 | (uint256 vaultContrib, uint256 totalContrib) = _getVaultShares(
879 | _vault,
880 | startDrawIdInclusive,
881 | drawIdPriorToShutdown
882 | );
883 |
884 | (uint256 _userTwab, uint256 _vaultTwabTotalSupply) = getVaultUserBalanceAndTotalSupplyTwab(
885 | _vault,
886 | _account,
887 | startDrawIdInclusive,
888 | drawIdPriorToShutdown
889 | );
890 |
891 | if (_vaultTwabTotalSupply == 0 || totalContrib == 0) {
892 | return UD60x18.wrap(0);
893 | }
894 |
895 | // first division purposely done before multiplication to avoid overflow
896 | return convert(vaultContrib)
897 | .div(convert(totalContrib))
898 | .mul(convert(_userTwab))
899 | .div(convert(_vaultTwabTotalSupply));
900 | }
901 |
902 | /// @notice Returns the shutdown balance for a given vault and account. The prize pool must already be shutdown.
903 | /// @dev The shutdown balance is the amount of prize tokens that a user can claim after the prize pool has been shutdown.
904 | /// @dev The shutdown balance is calculated using the user's TWAB and the total supply TWAB, whose time ranges are the
905 | /// grand prize period prior to the shutdown timestamp.
906 | /// @param _vault The vault to check
907 | /// @param _account The account to check
908 | /// @return The shutdown balance for the given vault and account
909 | function shutdownBalanceOf(address _vault, address _account) public returns (uint256) {
910 | if (!isShutdown()) {
911 | return 0;
912 | }
913 |
914 | Observation memory withdrawalObservation = _withdrawalObservations[_vault][_account];
915 | UD60x18 shutdownPortion;
916 | uint256 balance;
917 |
918 | // if we haven't withdrawn yet, add the portion of the shutdown balance
919 | if ((withdrawalObservation.available + withdrawalObservation.disbursed) == 0) {
920 | (balance, withdrawalObservation) = getShutdownInfo();
921 | shutdownPortion = computeShutdownPortion(_vault, _account);
922 | _shutdownPortions[_vault][_account] = shutdownPortion;
923 | } else {
924 | shutdownPortion = _shutdownPortions[_vault][_account];
925 | }
926 |
927 | if (shutdownPortion.unwrap() == 0) {
928 | return 0;
929 | }
930 |
931 | // if there are new rewards
932 | // current "draw id to award" observation - last withdraw observation
933 | Observation memory newestObs = _totalAccumulator.newestObservation();
934 | balance += (newestObs.available + newestObs.disbursed) - (withdrawalObservation.available + withdrawalObservation.disbursed);
935 |
936 | return convert(convert(balance).mul(shutdownPortion));
937 | }
938 |
939 | /// @notice Withdraws the shutdown balance for a given vault and sender
940 | /// @param _vault The eligible vault to withdraw the shutdown balance from
941 | /// @param _recipient The address to send the shutdown balance to
942 | /// @return The amount of prize tokens withdrawn
943 | function withdrawShutdownBalance(address _vault, address _recipient) external returns (uint256) {
944 | if (!isShutdown()) {
945 | revert PrizePoolNotShutdown();
946 | }
947 | uint256 balance = shutdownBalanceOf(_vault, msg.sender);
948 | _withdrawalObservations[_vault][msg.sender] = _totalAccumulator.newestObservation();
949 | if (balance > 0) {
950 | prizeToken.safeTransfer(_recipient, balance);
951 | _totalWithdrawn += uint128(balance);
952 | }
953 | return balance;
954 | }
955 |
956 | /// @notice Returns the open draw ID at the time of shutdown.
957 | /// @return The draw id
958 | function getShutdownDrawId() public view returns (uint24) {
959 | return getDrawId(shutdownAt());
960 | }
961 |
962 | /// @notice Returns the timestamp at which the prize pool will be considered inactive and shutdown
963 | /// @return The timestamp at which the prize pool will be considered inactive
964 | function shutdownAt() public view returns (uint256) {
965 | uint256 twabShutdownAt = drawOpensAt(getDrawId(twabController.lastObservationAt()));
966 | uint256 drawTimeoutAt_ = drawTimeoutAt();
967 | return drawTimeoutAt_ < twabShutdownAt ? drawTimeoutAt_ : twabShutdownAt;
968 | }
969 |
970 | /// @notice Returns whether the prize pool has been shutdown
971 | /// @return True if shutdown, false otherwise
972 | function isShutdown() public view returns (bool) {
973 | return block.timestamp >= shutdownAt();
974 | }
975 |
976 | /// @notice Returns the timestamp at which the prize pool will be considered inactive
977 | /// @return The timestamp at which the prize pool has timed out and becomes inactive
978 | function drawTimeoutAt() public view returns (uint256) {
979 | return drawClosesAt(_lastAwardedDrawId + drawTimeout);
980 | }
981 |
982 | /// @notice Returns the total prize tokens contributed between the given draw ids, inclusive.
983 | /// @param _startDrawIdInclusive Start draw id inclusive
984 | /// @param _endDrawIdInclusive End draw id inclusive
985 | /// @return The total prize tokens contributed by all vaults
986 | function getTotalContributedBetween(
987 | uint24 _startDrawIdInclusive,
988 | uint24 _endDrawIdInclusive
989 | ) public view returns (uint256) {
990 | return
991 | _totalAccumulator.getDisbursedBetween(
992 | _startDrawIdInclusive,
993 | _endDrawIdInclusive
994 | );
995 | }
996 |
997 | /// @notice Checks if the given user has won the prize for the specified tier in the given vault.
998 | /// @param _vault The address of the vault to check
999 | /// @param _user The address of the user to check for the prize
1000 | /// @param _tier The tier for which the prize is to be checked
1001 | /// @param _prizeIndex The prize index to check. Must be less than prize count for the tier
1002 | /// @return A boolean value indicating whether the user has won the prize or not
1003 | function isWinner(
1004 | address _vault,
1005 | address _user,
1006 | uint8 _tier,
1007 | uint32 _prizeIndex
1008 | ) public view returns (bool) {
1009 | uint24 lastAwardedDrawId_ = _lastAwardedDrawId;
1010 |
1011 | if (lastAwardedDrawId_ == 0) {
1012 | revert NoDrawsAwarded();
1013 | }
1014 | if (_tier >= numberOfTiers) {
1015 | revert InvalidTier(_tier, numberOfTiers);
1016 | }
1017 |
1018 | SD59x18 tierOdds = getTierOdds(_tier, numberOfTiers);
1019 | uint24 startDrawIdInclusive = computeRangeStartDrawIdInclusive(lastAwardedDrawId_, TierCalculationLib.estimatePrizeFrequencyInDraws(tierOdds, grandPrizePeriodDraws));
1020 |
1021 | uint32 tierPrizeCount = uint32(TierCalculationLib.prizeCount(_tier));
1022 |
1023 | if (_prizeIndex >= tierPrizeCount) {
1024 | revert InvalidPrizeIndex(_prizeIndex, tierPrizeCount, _tier);
1025 | }
1026 |
1027 | uint256 userSpecificRandomNumber = TierCalculationLib.calculatePseudoRandomNumber(
1028 | lastAwardedDrawId_,
1029 | _vault,
1030 | _user,
1031 | _tier,
1032 | _prizeIndex,
1033 | _winningRandomNumber
1034 | );
1035 |
1036 | SD59x18 vaultPortion = getVaultPortion(
1037 | _vault,
1038 | startDrawIdInclusive,
1039 | lastAwardedDrawId_
1040 | );
1041 |
1042 | (uint256 _userTwab, uint256 _vaultTwabTotalSupply) = getVaultUserBalanceAndTotalSupplyTwab(
1043 | _vault,
1044 | _user,
1045 | startDrawIdInclusive,
1046 | lastAwardedDrawId_
1047 | );
1048 |
1049 | return
1050 | TierCalculationLib.isWinner(
1051 | userSpecificRandomNumber,
1052 | _userTwab,
1053 | _vaultTwabTotalSupply,
1054 | vaultPortion,
1055 | tierOdds
1056 | );
1057 | }
1058 |
1059 | /// @notice Compute the start draw id for a range given the end draw id and range size
1060 | /// @param _endDrawIdInclusive The end draw id (inclusive) of the range
1061 | /// @param _rangeSize The size of the range
1062 | /// @return The start draw id (inclusive) of the range
1063 | function computeRangeStartDrawIdInclusive(uint24 _endDrawIdInclusive, uint24 _rangeSize) public pure returns (uint24) {
1064 | if (_rangeSize != 0) {
1065 | return _rangeSize > _endDrawIdInclusive ? 1 : _endDrawIdInclusive - _rangeSize + 1;
1066 | } else {
1067 | revert RangeSizeZero();
1068 | }
1069 | }
1070 |
1071 | /// @notice Returns the time-weighted average balance (TWAB) and the TWAB total supply for the specified user in
1072 | /// the given vault over a specified period.
1073 | /// @dev This function calculates the TWAB for a user by calling the getTwabBetween function of the TWAB controller
1074 | /// for a specified period of time.
1075 | /// @param _vault The address of the vault for which to get the TWAB.
1076 | /// @param _user The address of the user for which to get the TWAB.
1077 | /// @param _startDrawIdInclusive The starting draw for the range (inclusive)
1078 | /// @param _endDrawIdInclusive The end draw for the range (inclusive)
1079 | /// @return twab The TWAB for the specified user in the given vault over the specified period.
1080 | /// @return twabTotalSupply The TWAB total supply over the specified period.
1081 | function getVaultUserBalanceAndTotalSupplyTwab(
1082 | address _vault,
1083 | address _user,
1084 | uint24 _startDrawIdInclusive,
1085 | uint24 _endDrawIdInclusive
1086 | ) public view returns (uint256 twab, uint256 twabTotalSupply) {
1087 | uint48 _startTimestamp = drawOpensAt(_startDrawIdInclusive);
1088 | uint48 _endTimestamp = drawClosesAt(_endDrawIdInclusive);
1089 | twab = twabController.getTwabBetween(_vault, _user, _startTimestamp, _endTimestamp);
1090 | twabTotalSupply = twabController.getTotalSupplyTwabBetween(
1091 | _vault,
1092 | _startTimestamp,
1093 | _endTimestamp
1094 | );
1095 | }
1096 |
1097 | /// @notice Calculates the portion of the vault's contribution to the prize pool over a specified duration in draws.
1098 | /// @param _vault The address of the vault for which to calculate the portion.
1099 | /// @param _startDrawIdInclusive The starting draw ID (inclusive) of the draw range to calculate the contribution portion for.
1100 | /// @param _endDrawIdInclusive The ending draw ID (inclusive) of the draw range to calculate the contribution portion for.
1101 | /// @return The portion of the vault's contribution to the prize pool over the specified duration in draws.
1102 | function getVaultPortion(
1103 | address _vault,
1104 | uint24 _startDrawIdInclusive,
1105 | uint24 _endDrawIdInclusive
1106 | ) public view returns (SD59x18) {
1107 | if (_vault == DONATOR) {
1108 | return sd(0);
1109 | }
1110 |
1111 | (uint256 vaultContributed, uint256 totalContributed) = _getVaultShares(_vault, _startDrawIdInclusive, _endDrawIdInclusive);
1112 |
1113 | if (totalContributed == 0) {
1114 | return sd(0);
1115 | }
1116 |
1117 | return sd(
1118 | SafeCast.toInt256(
1119 | vaultContributed
1120 | )
1121 | ).div(sd(SafeCast.toInt256(totalContributed)));
1122 | }
1123 |
1124 | function _getVaultShares(
1125 | address _vault,
1126 | uint24 _startDrawIdInclusive,
1127 | uint24 _endDrawIdInclusive
1128 | ) internal view returns (uint256 shares, uint256 totalSupply) {
1129 | uint256 totalContributed = _totalAccumulator.getDisbursedBetween(
1130 | _startDrawIdInclusive,
1131 | _endDrawIdInclusive
1132 | );
1133 |
1134 | uint256 totalDonated = _vaultAccumulator[DONATOR].getDisbursedBetween(_startDrawIdInclusive, _endDrawIdInclusive);
1135 |
1136 | totalSupply = totalContributed - totalDonated;
1137 |
1138 | shares = _vaultAccumulator[_vault].getDisbursedBetween(
1139 | _startDrawIdInclusive,
1140 | _endDrawIdInclusive
1141 | );
1142 | }
1143 |
1144 | function _accountedBalance(Observation memory _observation) internal view returns (uint256) {
1145 | // obs.disbursed includes the reserve, prizes, and prize liquidity
1146 | // obs.disbursed is the total amount of tokens all-time contributed by vaults and released. These tokens may:
1147 | // - still be held for future prizes
1148 | // - have been given as prizes
1149 | // - have been captured as fees
1150 |
1151 | // obs.available is the total number of tokens that WILL be disbursed in the future.
1152 | // _directlyContributedReserve are tokens that have been contributed directly to the reserve
1153 | // totalWithdrawn represents all tokens that have been withdrawn as prizes or rewards
1154 |
1155 | return (_observation.available + _observation.disbursed) + uint256(_directlyContributedReserve) - uint256(_totalWithdrawn);
1156 | }
1157 |
1158 | /// @notice Modifier that requires the prize pool not to be shutdown
1159 | modifier notShutdown() {
1160 | if (isShutdown()) {
1161 | revert PrizePoolShutdown();
1162 | }
1163 | _;
1164 | }
1165 | }
1166 |
--------------------------------------------------------------------------------