├── .nvmrc ├── .prettierignore ├── .mocharc.json ├── banner.png ├── scripts ├── package-version.sh └── verify.js ├── helpers.ts ├── .solcover.js ├── hardhat ├── prizeDistributions.js └── tsunami-tasks.js ├── test ├── helpers │ ├── call.ts │ ├── fillPrizeTiersWithZeros.ts │ ├── printUtils.js │ ├── getEvents.js │ ├── increaseTime.ts │ ├── chaiMatchers.js │ ├── signPermit.ts │ └── delegateSignature.ts ├── types.ts ├── features │ ├── oracle.test.js │ ├── ticket.test.js │ └── support │ │ └── PoolEnv.js ├── libraries │ ├── ExtendedSafeCast.test.ts │ ├── ObservationLib.test.ts │ ├── DrawRingBufferLib.test.ts │ └── OverflowSafeComparatorLib.test.ts ├── prize-pool │ ├── StakePrizePool.test.ts │ └── YieldSourcePrizePool.test.ts └── ControlledToken.test.ts ├── contracts ├── test │ ├── YieldSourceStub.sol │ ├── PrizeSplitStrategyHarness.sol │ ├── ERC721Mintable.sol │ ├── libraries │ │ ├── ExtendedSafeCastLibHarness.sol │ │ ├── OverflowSafeComparatorLibHarness.sol │ │ ├── DrawRingBufferLibHarness.sol │ │ └── ObservationLibHarness.sol │ ├── PrizeSplitHarness.sol │ ├── ReserveHarness.sol │ ├── DrawBufferHarness.sol │ ├── DrawRingBufferExposed.sol │ ├── ERC20Mintable.sol │ ├── EIP2612PermitMintable.sol │ ├── DrawBeaconHarness.sol │ ├── RNGServiceMock.sol │ ├── PrizePoolHarness.sol │ ├── DrawCalculatorHarness.sol │ ├── DrawCalculatorV2Harness.sol │ ├── TicketHarness.sol │ └── TwabLibraryExposed.sol ├── external │ └── compound │ │ ├── ICompLike.sol │ │ └── CTokenInterface.sol ├── interfaces │ ├── IStrategy.sol │ ├── IControlledToken.sol │ ├── IReserve.sol │ ├── IPrizeDistributionSource.sol │ ├── IDrawCalculator.sol │ ├── IDrawBuffer.sol │ ├── IPrizeDistributionBuffer.sol │ ├── IPrizeSplit.sol │ ├── IPrizeDistributor.sol │ ├── IDrawBeacon.sol │ └── ITicket.sol ├── libraries │ ├── DrawRingBufferLib.sol │ ├── ExtendedSafeCastLib.sol │ ├── RingBufferLib.sol │ ├── OverflowSafeComparatorLib.sol │ └── ObservationLib.sol ├── prize-pool │ ├── StakePrizePool.sol │ └── YieldSourcePrizePool.sol ├── prize-strategy │ ├── PrizeSplitStrategy.sol │ └── PrizeSplit.sol ├── ControlledToken.sol ├── permit │ └── EIP2612PermitAndDeposit.sol ├── PrizeDistributor.sol └── DrawBuffer.sol ├── .envrc.example ├── .prettierrc ├── tsconfig.json ├── .solhint.json ├── .gitignore ├── .github └── workflows │ └── main.yml ├── templates └── contract.hbs ├── hardhat.config.ts ├── hardhat.network.ts ├── package.json ├── README.md └── deploy └── deploy.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | types 2 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 120000 3 | } -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pooltogether/v4-core/HEAD/banner.png -------------------------------------------------------------------------------- /scripts/package-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo $(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') -------------------------------------------------------------------------------- /helpers.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | 3 | export const increaseTime = async (provider: providers.JsonRpcProvider, time: number) => { 4 | await provider.send('evm_increaseTime', [time]); 5 | await provider.send('evm_mine', []); 6 | }; 7 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mocha: { reporter: 'mocha-junit-reporter' }, 3 | providerOptions: { 4 | network_id: 1337, 5 | _chainId: 1337, 6 | _chainIdRpc: 1337, 7 | }, 8 | skipFiles: ['external', 'test'], 9 | }; 10 | -------------------------------------------------------------------------------- /hardhat/prizeDistributions.js: -------------------------------------------------------------------------------- 1 | const { utils } = require('ethers'); 2 | 3 | const tiers = [ 4 | utils.parseEther('0.9'), 5 | utils.parseEther('0.1'), 6 | utils.parseEther('0.1'), 7 | utils.parseEther('0.1'), 8 | ]; 9 | 10 | module.exports = tiers; 11 | -------------------------------------------------------------------------------- /test/helpers/call.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from '@ethersproject/contracts'; 2 | 3 | export const call = async ( 4 | contract: Contract, 5 | functionName: string, 6 | ...args: (string | undefined)[] 7 | ) => { 8 | return await contract.callStatic[functionName](...args); 9 | }; 10 | -------------------------------------------------------------------------------- /contracts/test/YieldSourceStub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/yield-source-interface/contracts/IYieldSource.sol"; 6 | 7 | interface YieldSourceStub is IYieldSource { 8 | function canAwardExternal(address _externalToken) external view returns (bool); 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/fillPrizeTiersWithZeros.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers'; 2 | 3 | export function fillPrizeTiersWithZeros(tiers: BigNumber[]): BigNumber[]{ 4 | const existingLength = tiers.length 5 | const lengthOfZeroesRequired = 16 - existingLength 6 | return [...tiers, ...Array(lengthOfZeroesRequired).fill(BigNumber.from(0))] 7 | } 8 | -------------------------------------------------------------------------------- /contracts/external/compound/ICompLike.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | interface ICompLike is IERC20 { 8 | function getCurrentVotes(address account) external view returns (uint96); 9 | 10 | function delegate(address delegate) external; 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers/printUtils.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk") 2 | 3 | function dim() { 4 | console.log(chalk.dim.call(chalk, ...arguments)) 5 | } 6 | 7 | function yellow() { 8 | console.log(chalk.yellow.call(chalk, ...arguments)) 9 | } 10 | 11 | function green() { 12 | console.log(chalk.green.call(chalk, ...arguments)) 13 | } 14 | 15 | module.exports = { 16 | dim, 17 | yellow, 18 | green 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/getEvents.js: -------------------------------------------------------------------------------- 1 | const hardhat = require('hardhat') 2 | 3 | async function getEvents(contract, tx) { 4 | let receipt = await hardhat.ethers.provider.getTransactionReceipt(tx.hash) 5 | return receipt.logs.reduce((parsedEvents, log) => { 6 | try { 7 | parsedEvents.push(contract.interface.parseLog(log)) 8 | } catch (e) {} 9 | return parsedEvents 10 | }, []) 11 | } 12 | 13 | module.exports = { getEvents } 14 | -------------------------------------------------------------------------------- /.envrc.example: -------------------------------------------------------------------------------- 1 | # Used to deploy to testnets 2 | export HDWALLET_MNEMONIC='' 3 | 4 | # Used to deploy to testnets 5 | export INFURA_API_KEY='' 6 | 7 | # Used for verifying contracts on Etherscan 8 | export ETHERSCAN_API_KEY='' 9 | 10 | # Required for forking 11 | export ALCHEMY_URL='' 12 | export FORK_ENABLED='' 13 | 14 | # Used to hide deploy logs 15 | export HIDE_DEPLOY_LOG='' 16 | 17 | # Used to generate a gas usage report 18 | export REPORT_GAS='' 19 | export COINMARKETCAP_API_KEY='' 20 | -------------------------------------------------------------------------------- /contracts/test/PrizeSplitStrategyHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../prize-strategy/PrizeSplitStrategy.sol"; 6 | 7 | contract PrizeSplitStrategyHarness is PrizeSplitStrategy { 8 | constructor(address _owner, IPrizePool _prizePool) PrizeSplitStrategy(_owner, _prizePool) {} 9 | 10 | function awardPrizeSplitAmount(address target, uint256 amount) external { 11 | return _awardPrizeSplitAmount(target, amount); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | 3 | export type PrizeDistribution = { 4 | matchCardinality: BigNumber; 5 | numberOfPicks: BigNumber; 6 | tiers: BigNumber[]; 7 | bitRangeSize: BigNumber; 8 | prize: BigNumber; 9 | startTimestampOffset: BigNumber; 10 | endTimestampOffset: BigNumber; 11 | maxPicksPerUser: BigNumber; 12 | expiryDuration: BigNumber; 13 | }; 14 | 15 | export type Draw = { drawId: BigNumber, winningRandomNumber: BigNumber, timestamp: BigNumber } 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "overrides": [ 5 | { 6 | "files": "*.*.{ts,js}", 7 | "options": { 8 | "parser": "typescript", 9 | "trailingComma": "all", 10 | "arrowParens": "always", 11 | "singleQuote": true 12 | } 13 | }, 14 | { 15 | "files": "*.sol", 16 | "options": { 17 | "bracketSpacing": true, 18 | "explicitTypes": "always" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "resolveJsonModule": true, 10 | "downlevelIteration": true 11 | }, 12 | "include": [ 13 | "hardhat.config.ts", 14 | "hardhat.network.ts", 15 | "./deploy", 16 | "./scripts", 17 | "./test", 18 | "types/**/*", 19 | "Constant.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /contracts/test/ERC721Mintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | 7 | /** 8 | * @dev Extension of {ERC721} for Minting/Burning 9 | */ 10 | contract ERC721Mintable is ERC721 { 11 | constructor() ERC721("ERC 721", "NFT") {} 12 | 13 | /** 14 | * @dev See {ERC721-_mint}. 15 | */ 16 | function mint(address to, uint256 tokenId) public { 17 | _mint(to, tokenId); 18 | } 19 | 20 | /** 21 | * @dev See {ERC721-_burn}. 22 | */ 23 | function burn(uint256 tokenId) public { 24 | _burn(tokenId); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/IStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | interface IStrategy { 6 | /** 7 | * @notice Emit when a strategy captures award amount from PrizePool. 8 | * @param totalPrizeCaptured Total prize captured from the PrizePool 9 | */ 10 | event Distributed(uint256 totalPrizeCaptured); 11 | 12 | /** 13 | * @notice Capture the award balance and distribute to prize splits. 14 | * @dev Permissionless function to initialize distribution of interst 15 | * @return Prize captured from PrizePool 16 | */ 17 | function distribute() external returns (uint256); 18 | } 19 | -------------------------------------------------------------------------------- /test/helpers/increaseTime.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | 3 | export async function advanceBlock(provider: providers.JsonRpcProvider) { 4 | return provider.send("evm_mine", []); 5 | } 6 | 7 | export const increaseTime = async (provider: providers.JsonRpcProvider, time: number, advance: Boolean = true) => { 8 | await provider.send('evm_increaseTime', [time]); 9 | if (advance) await advanceBlock(provider); 10 | }; 11 | 12 | 13 | export async function setTime(provider: providers.JsonRpcProvider, time: number, advance: Boolean = true) { 14 | await provider.send("evm_setNextBlockTimestamp", [time]); 15 | if (advance) await advanceBlock(provider); 16 | } -------------------------------------------------------------------------------- /contracts/test/libraries/ExtendedSafeCastLibHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../../libraries/ExtendedSafeCastLib.sol"; 6 | 7 | contract ExtendedSafeCastLibHarness { 8 | using ExtendedSafeCastLib for uint256; 9 | 10 | function toUint104(uint256 value) external pure returns (uint104) { 11 | return value.toUint104(); 12 | } 13 | 14 | function toUint208(uint256 value) external pure returns (uint208) { 15 | return value.toUint208(); 16 | } 17 | 18 | function toUint224(uint256 value) external pure returns (uint224) { 19 | return value.toUint224(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": [], 4 | "rules": { 5 | "func-order": "off", 6 | "mark-callable-contracts": "off", 7 | "no-empty-blocks": "off", 8 | "compiler-version": ["error", "0.8.6"], 9 | "private-vars-leading-underscore": "off", 10 | "code-complexity": "warn", 11 | "const-name-snakecase": "warn", 12 | "function-max-lines": "warn", 13 | "func-visibility": ["warn", { "ignoreConstructors": true }], 14 | "max-line-length": ["warn", 160], 15 | "avoid-suicide": "error", 16 | "avoid-sha3": "warn", 17 | "not-rely-on-time": "off", 18 | "reason-string": ["warn", { "maxLength": 64 }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/releases 3 | !/.yarn/plugins 4 | !/.yarn/sdks 5 | 6 | # Swap the comments on the following lines if you don't wish to use zero-installs 7 | # Documentation here: https://yarnpkg.com/features/zero-installs 8 | #!/.yarn/cache 9 | /.pnp.* 10 | 11 | .envrc 12 | 13 | abis 14 | artifacts 15 | cache 16 | node_modules 17 | types 18 | 19 | # Build/Deploy Process 20 | build 21 | .build-openzeppelin 22 | deployments/local* 23 | deployments/fork* 24 | deployments/hardhat* 25 | 26 | # Tests 27 | test-results.xml 28 | 29 | # Solidity Coverage 30 | coverage 31 | coverage.json 32 | .history 33 | 34 | # OSX 35 | .DS_Store 36 | 37 | # Development Notes 38 | Networks.md 39 | *.dev.md 40 | 41 | # Docs 42 | docs -------------------------------------------------------------------------------- /contracts/test/PrizeSplitHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../prize-strategy/PrizeSplit.sol"; 6 | import "../interfaces/IControlledToken.sol"; 7 | 8 | contract PrizeSplitHarness is PrizeSplit { 9 | constructor(address _owner) Ownable(_owner) {} 10 | 11 | function _awardPrizeSplitAmount(address target, uint256 amount) internal override { 12 | emit PrizeSplitAwarded(target, amount, IControlledToken(address(0))); 13 | } 14 | 15 | function awardPrizeSplitAmount(address target, uint256 amount) external { 16 | return _awardPrizeSplitAmount(target, amount); 17 | } 18 | 19 | function getPrizePool() external pure override returns (IPrizePool) { 20 | return IPrizePool(address(0)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/test/ReserveHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../Reserve.sol"; 6 | import "./ERC20Mintable.sol"; 7 | 8 | contract ReserveHarness is Reserve { 9 | constructor(address _owner, IERC20 _token) Reserve(_owner, _token) {} 10 | 11 | function setObservationsAt(ObservationLib.Observation[] calldata observations) external { 12 | for (uint256 i = 0; i < observations.length; i++) { 13 | reserveAccumulators[i] = observations[i]; 14 | } 15 | 16 | nextIndex = uint24(observations.length); 17 | cardinality = uint24(observations.length); 18 | } 19 | 20 | function doubleCheckpoint(ERC20Mintable _token, uint256 _amount) external { 21 | _checkpoint(); 22 | _token.mint(address(this), _amount); 23 | _checkpoint(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/helpers/chaiMatchers.js: -------------------------------------------------------------------------------- 1 | const ethers = require('ethers'); 2 | const { Assertion } = require('chai'); 3 | 4 | Assertion.addMethod('equalish', function (value, difference = 10) { 5 | var obj = this._obj; 6 | 7 | // TODO Fix bug even when it's a BigNumber 8 | // first, our instanceof check, shortcut 9 | // new Assertion(obj).to.be.instanceof(ethers.BigNumber); 10 | // new Assertion(value).to.be.instanceof(ethers.BigNumber); 11 | 12 | let delta; 13 | if (obj.lt(value)) { 14 | delta = value.sub(obj); 15 | } else { 16 | delta = obj.sub(value); 17 | } 18 | 19 | this.assert( 20 | delta.lte(difference), 21 | `expected ${obj.toString()} to be within ${difference} of #{exp} but got #{act}`, 22 | `expected ${obj.toString()} to not be within #{act}`, 23 | value.toString(), // expected 24 | obj.toString(), // actual 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /contracts/test/libraries/OverflowSafeComparatorLibHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../../libraries/OverflowSafeComparatorLib.sol"; 6 | 7 | contract OverflowSafeComparatorLibHarness { 8 | using OverflowSafeComparatorLib for uint32; 9 | 10 | function ltHarness( 11 | uint32 _a, 12 | uint32 _b, 13 | uint32 _timestamp 14 | ) external pure returns (bool) { 15 | return _a.lt(_b, _timestamp); 16 | } 17 | 18 | function lteHarness( 19 | uint32 _a, 20 | uint32 _b, 21 | uint32 _timestamp 22 | ) external pure returns (bool) { 23 | return _a.lte(_b, _timestamp); 24 | } 25 | 26 | function checkedSub( 27 | uint256 _a, 28 | uint256 _b, 29 | uint256 _timestamp 30 | ) external pure returns (uint32) { 31 | return uint32(_a).checkedSub(uint32(_b), uint32(_timestamp)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/test/DrawBufferHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../DrawBuffer.sol"; 6 | import "../interfaces/IDrawBeacon.sol"; 7 | 8 | contract DrawBufferHarness is DrawBuffer { 9 | constructor(address owner, uint8 card) DrawBuffer(owner, card) {} 10 | 11 | function addMultipleDraws( 12 | uint256 _start, 13 | uint256 _numberOfDraws, 14 | uint32 _timestamp, 15 | uint256 _winningRandomNumber 16 | ) external { 17 | for (uint256 index = _start; index <= _numberOfDraws; index++) { 18 | IDrawBeacon.Draw memory _draw = IDrawBeacon.Draw({ 19 | winningRandomNumber: _winningRandomNumber, 20 | drawId: uint32(index), 21 | timestamp: _timestamp, 22 | beaconPeriodSeconds: 10, 23 | beaconPeriodStartedAt: 20 24 | }); 25 | 26 | _pushDraw(_draw); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/external/compound/CTokenInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | interface CTokenInterface is IERC20 { 8 | function decimals() external view returns (uint8); 9 | 10 | function totalSupply() external view override returns (uint256); 11 | 12 | function underlying() external view returns (address); 13 | 14 | function balanceOfUnderlying(address owner) external returns (uint256); 15 | 16 | function supplyRatePerBlock() external returns (uint256); 17 | 18 | function exchangeRateCurrent() external returns (uint256); 19 | 20 | function mint(uint256 mintAmount) external returns (uint256); 21 | 22 | function redeem(uint256 amount) external returns (uint256); 23 | 24 | function balanceOf(address user) external view override returns (uint256); 25 | 26 | function redeemUnderlying(uint256 redeemAmount) external returns (uint256); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Coveralls 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 16.x 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: 16.x 14 | - name: yarn, compile, hint, coverage 15 | continue-on-error: false 16 | run: | 17 | yarn 18 | yarn compile 19 | yarn hint 20 | yarn coverage 21 | - name: Coverage Artifacts Upload 22 | uses: actions/upload-artifact@v2 23 | with: 24 | name: test-results 25 | path: test-results.xml 26 | - name: Coverage Artifacts Download 27 | uses: actions/download-artifact@v2 28 | with: 29 | name: test-results 30 | - name: Coveralls 31 | uses: coverallsapp/github-action@master 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /contracts/test/DrawRingBufferExposed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../libraries/DrawRingBufferLib.sol"; 6 | 7 | /** 8 | * @title Expose the DrawRingBufferLibrary for unit tests 9 | * @author PoolTogether Inc. 10 | */ 11 | contract DrawRingBufferLibExposed { 12 | using DrawRingBufferLib for DrawRingBufferLib.Buffer; 13 | 14 | uint16 public constant MAX_CARDINALITY = 256; 15 | DrawRingBufferLib.Buffer internal bufferMetadata; 16 | 17 | constructor(uint8 _cardinality) { 18 | bufferMetadata.cardinality = _cardinality; 19 | } 20 | 21 | function _push(DrawRingBufferLib.Buffer memory _buffer, uint32 _drawId) 22 | external 23 | pure 24 | returns (DrawRingBufferLib.Buffer memory) 25 | { 26 | return DrawRingBufferLib.push(_buffer, _drawId); 27 | } 28 | 29 | function _getIndex(DrawRingBufferLib.Buffer memory _buffer, uint32 _drawId) 30 | external 31 | pure 32 | returns (uint32) 33 | { 34 | return DrawRingBufferLib.getIndex(_buffer, _drawId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/test/ERC20Mintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | /** 8 | * @dev Extension of {ERC20} that adds a set of accounts with the {MinterRole}, 9 | * which have permission to mint (create) new tokens as they see fit. 10 | * 11 | * At construction, the deployer of the contract is the only minter. 12 | */ 13 | contract ERC20Mintable is ERC20 { 14 | constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} 15 | 16 | /** 17 | * @dev See {ERC20-_mint}. 18 | * 19 | * Requirements: 20 | * 21 | * - the caller must have the {MinterRole}. 22 | */ 23 | function mint(address account, uint256 amount) public { 24 | _mint(account, amount); 25 | } 26 | 27 | function burn(address account, uint256 amount) public returns (bool) { 28 | _burn(account, amount); 29 | return true; 30 | } 31 | 32 | function masterTransfer( 33 | address from, 34 | address to, 35 | uint256 amount 36 | ) public { 37 | _transfer(from, to, amount); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/test/libraries/DrawRingBufferLibHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../../libraries/DrawRingBufferLib.sol"; 6 | 7 | /** 8 | * @title Expose the DrawRingBufferLib for unit tests 9 | * @author PoolTogether Inc. 10 | */ 11 | contract DrawRingBufferLibHarness { 12 | using DrawRingBufferLib for DrawRingBufferLib.Buffer; 13 | 14 | uint16 public constant MAX_CARDINALITY = 256; 15 | DrawRingBufferLib.Buffer internal bufferMetadata; 16 | 17 | constructor(uint8 _cardinality) { 18 | bufferMetadata.cardinality = _cardinality; 19 | } 20 | 21 | function _push(DrawRingBufferLib.Buffer memory _buffer, uint32 _drawId) 22 | external 23 | pure 24 | returns (DrawRingBufferLib.Buffer memory) 25 | { 26 | return DrawRingBufferLib.push(_buffer, _drawId); 27 | } 28 | 29 | function _getIndex(DrawRingBufferLib.Buffer memory _buffer, uint32 _drawId) 30 | external 31 | pure 32 | returns (uint32) 33 | { 34 | return DrawRingBufferLib.getIndex(_buffer, _drawId); 35 | } 36 | 37 | function _isInitialized(DrawRingBufferLib.Buffer memory _buffer) external pure returns (bool) { 38 | return DrawRingBufferLib.isInitialized(_buffer); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/test/EIP2612PermitMintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; 6 | 7 | /** 8 | * @dev Extension of {ERC20Permit} that adds a set of accounts with the {MinterRole}, 9 | * which have permission to mint (create) new tokens as they see fit. 10 | * 11 | * At construction, the deployer of the contract is the only minter. 12 | */ 13 | contract EIP2612PermitMintable is ERC20Permit { 14 | constructor(string memory _name, string memory _symbol) 15 | ERC20(_name, _symbol) 16 | ERC20Permit(_name) 17 | {} 18 | 19 | /** 20 | * @dev See {ERC20-_mint}. 21 | * 22 | * Requirements: 23 | * 24 | * - the caller must have the {MinterRole}. 25 | */ 26 | function mint(address account, uint256 amount) public returns (bool) { 27 | _mint(account, amount); 28 | return true; 29 | } 30 | 31 | function burn(address account, uint256 amount) public returns (bool) { 32 | _burn(account, amount); 33 | return true; 34 | } 35 | 36 | function masterTransfer( 37 | address from, 38 | address to, 39 | uint256 amount 40 | ) public { 41 | _transfer(from, to, amount); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/test/DrawBeaconHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/pooltogether-rng-contracts/contracts/RNGInterface.sol"; 6 | 7 | import "../DrawBeacon.sol"; 8 | import "../interfaces/IDrawBuffer.sol"; 9 | 10 | contract DrawBeaconHarness is DrawBeacon { 11 | constructor( 12 | address _owner, 13 | IDrawBuffer _drawBuffer, 14 | RNGInterface _rng, 15 | uint32 _nextDrawId, 16 | uint64 _beaconPeriodStart, 17 | uint32 _drawPeriodSeconds, 18 | uint32 _rngTimeout 19 | ) DrawBeacon(_owner, _drawBuffer, _rng, _nextDrawId, _beaconPeriodStart, _drawPeriodSeconds, _rngTimeout) {} 20 | 21 | uint64 internal time; 22 | 23 | function setCurrentTime(uint64 _time) external { 24 | time = _time; 25 | } 26 | 27 | function _currentTime() internal view override returns (uint64) { 28 | return time; 29 | } 30 | 31 | function currentTime() external view returns (uint64) { 32 | return _currentTime(); 33 | } 34 | 35 | function _currentTimeInternal() external view returns (uint64) { 36 | return super._currentTime(); 37 | } 38 | 39 | function setRngRequest(uint32 requestId, uint32 lockBlock) external { 40 | rngRequest.id = requestId; 41 | rngRequest.lockBlock = lockBlock; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/test/RNGServiceMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/pooltogether-rng-contracts/contracts/RNGInterface.sol"; 6 | 7 | contract RNGServiceMock is RNGInterface { 8 | uint256 internal random; 9 | address internal feeToken; 10 | uint256 internal requestFee; 11 | 12 | function getLastRequestId() external pure override returns (uint32 requestId) { 13 | return 1; 14 | } 15 | 16 | function setRequestFee(address _feeToken, uint256 _requestFee) external { 17 | feeToken = _feeToken; 18 | requestFee = _requestFee; 19 | } 20 | 21 | /// @return _feeToken 22 | /// @return _requestFee 23 | function getRequestFee() 24 | external 25 | view 26 | override 27 | returns (address _feeToken, uint256 _requestFee) 28 | { 29 | return (feeToken, requestFee); 30 | } 31 | 32 | function setRandomNumber(uint256 _random) external { 33 | random = _random; 34 | } 35 | 36 | function requestRandomNumber() external pure override returns (uint32, uint32) { 37 | return (1, 1); 38 | } 39 | 40 | function isRequestComplete(uint32) external pure override returns (bool) { 41 | return true; 42 | } 43 | 44 | function randomNumber(uint32) external view override returns (uint256) { 45 | return random; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/test/libraries/ObservationLibHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../../libraries/ObservationLib.sol"; 6 | 7 | /// @title Time-Weighted Average Balance Library 8 | /// @notice This library allows you to efficiently track a user's historic balance. You can get a 9 | /// @author PoolTogether Inc. 10 | contract ObservationLibHarness { 11 | /// @notice The maximum number of twab entries 12 | uint24 public constant MAX_CARDINALITY = 16777215; // 2**24 13 | 14 | ObservationLib.Observation[MAX_CARDINALITY] observations; 15 | 16 | function setObservations(ObservationLib.Observation[] calldata _observations) external { 17 | for (uint256 i = 0; i < _observations.length; i++) { 18 | observations[i] = _observations[i]; 19 | } 20 | } 21 | 22 | function binarySearch( 23 | uint24 _observationIndex, 24 | uint24 _oldestObservationIndex, 25 | uint32 _target, 26 | uint24 _cardinality, 27 | uint32 _time 28 | ) 29 | external 30 | view 31 | returns ( 32 | ObservationLib.Observation memory beforeOrAt, 33 | ObservationLib.Observation memory atOrAfter 34 | ) 35 | { 36 | return 37 | ObservationLib.binarySearch( 38 | observations, 39 | _observationIndex, 40 | _oldestObservationIndex, 41 | _target, 42 | _cardinality, 43 | _time 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/features/oracle.test.js: -------------------------------------------------------------------------------- 1 | const { PoolEnv } = require('./support/PoolEnv'); 2 | const ethers = require('ethers'); 3 | const { fillPrizeTiersWithZeros } = require('../helpers/fillPrizeTiersWithZeros'); 4 | 5 | const toWei = (val) => ethers.utils.parseEther('' + val); 6 | 7 | describe('Oracle jobs', () => { 8 | let env; 9 | 10 | beforeEach(async () => { 11 | env = new PoolEnv(); 12 | await env.ready(); 13 | }); 14 | 15 | it('should be able to trigger the beacon', async () => { 16 | await env.draw({ randomNumber: 1 }); 17 | await env.expectDrawRandomNumber({ drawId: 1, randomNumber: 1 }); 18 | }); 19 | 20 | it('should be able to push new draw settings', async () => { 21 | await env.poolAccrues({ tickets: 10 }); 22 | await env.draw({ randomNumber: 1 }); 23 | bitRangeSize = 2; 24 | matchCardinality = 2; 25 | numberOfPicks = toWei(1); 26 | tiers = [ethers.utils.parseUnits('1', 9)]; 27 | tiers = fillPrizeTiersWithZeros(tiers); 28 | prize = toWei(10); 29 | startTimestampOffset = 1; 30 | endTimestampOffset = 2; 31 | maxPicksPerUser = 1000; 32 | expiryDuration = 100; 33 | 34 | await env.pushPrizeDistribution({ 35 | drawId: 1, 36 | bitRangeSize, 37 | matchCardinality, 38 | startTimestampOffset, 39 | endTimestampOffset, 40 | expiryDuration, 41 | numberOfPicks, 42 | tiers, 43 | prize, 44 | maxPicksPerUser, 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/helpers/signPermit.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers'; 2 | 3 | const domainSchema = [ 4 | { name: 'name', type: 'string' }, 5 | { name: 'version', type: 'string' }, 6 | { name: 'chainId', type: 'uint256' }, 7 | { name: 'verifyingContract', type: 'address' }, 8 | ]; 9 | 10 | const permitSchema = [ 11 | { name: 'owner', type: 'address' }, 12 | { name: 'spender', type: 'address' }, 13 | { name: 'value', type: 'uint256' }, 14 | { name: 'nonce', type: 'uint256' }, 15 | { name: 'deadline', type: 'uint256' }, 16 | ]; 17 | 18 | export const signPermit = async (signer: any, domain: any, message: any) => { 19 | let myAddr = signer.address; 20 | 21 | if (myAddr.toLowerCase() !== message.owner.toLowerCase()) { 22 | throw `signPermit: address of signer does not match owner address in message`; 23 | } 24 | 25 | if (message.nonce === undefined) { 26 | let tokenAbi = ['function nonces(address owner) view returns (uint)']; 27 | 28 | let tokenContract = new Contract(domain.verifyingContract, tokenAbi, signer); 29 | 30 | let nonce = await tokenContract.nonces(myAddr); 31 | 32 | message = { ...message, nonce: nonce.toString() }; 33 | } 34 | 35 | let typedData = { 36 | types: { 37 | EIP712Domain: domainSchema, 38 | Permit: permitSchema, 39 | }, 40 | primaryType: 'Permit', 41 | domain, 42 | message, 43 | }; 44 | 45 | let sig; 46 | 47 | if (signer && signer.provider) { 48 | try { 49 | sig = await signer.provider.send('eth_signTypedData', [myAddr, typedData]); 50 | } catch (e: any) { 51 | if (/is not supported/.test(e.message)) { 52 | sig = await signer.provider.send('eth_signTypedData_v4', [myAddr, typedData]); 53 | } 54 | } 55 | } 56 | 57 | return { domain, message, sig }; 58 | } 59 | -------------------------------------------------------------------------------- /contracts/interfaces/IControlledToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | /** @title IControlledToken 8 | * @author PoolTogether Inc Team 9 | * @notice ERC20 Tokens with a controller for minting & burning. 10 | */ 11 | interface IControlledToken is IERC20 { 12 | 13 | /** 14 | @notice Interface to the contract responsible for controlling mint/burn 15 | */ 16 | function controller() external view returns (address); 17 | 18 | /** 19 | * @notice Allows the controller to mint tokens for a user account 20 | * @dev May be overridden to provide more granular control over minting 21 | * @param user Address of the receiver of the minted tokens 22 | * @param amount Amount of tokens to mint 23 | */ 24 | function controllerMint(address user, uint256 amount) external; 25 | 26 | /** 27 | * @notice Allows the controller to burn tokens from a user account 28 | * @dev May be overridden to provide more granular control over burning 29 | * @param user Address of the holder account to burn tokens from 30 | * @param amount Amount of tokens to burn 31 | */ 32 | function controllerBurn(address user, uint256 amount) external; 33 | 34 | /** 35 | * @notice Allows an operator via the controller to burn tokens on behalf of a user account 36 | * @dev May be overridden to provide more granular control over operator-burning 37 | * @param operator Address of the operator performing the burn action via the controller contract 38 | * @param user Address of the holder account to burn tokens from 39 | * @param amount Amount of tokens to burn 40 | */ 41 | function controllerBurnFrom( 42 | address operator, 43 | address user, 44 | uint256 amount 45 | ) external; 46 | } 47 | -------------------------------------------------------------------------------- /contracts/interfaces/IReserve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | interface IReserve { 8 | /** 9 | * @notice Emit when checkpoint is created. 10 | * @param reserveAccumulated Total depsosited 11 | * @param withdrawAccumulated Total withdrawn 12 | */ 13 | 14 | event Checkpoint(uint256 reserveAccumulated, uint256 withdrawAccumulated); 15 | /** 16 | * @notice Emit when the withdrawTo function has executed. 17 | * @param recipient Address receiving funds 18 | * @param amount Amount of tokens transfered. 19 | */ 20 | event Withdrawn(address indexed recipient, uint256 amount); 21 | 22 | /** 23 | * @notice Create observation checkpoint in ring bufferr. 24 | * @dev Calculates total desposited tokens since last checkpoint and creates new accumulator checkpoint. 25 | */ 26 | function checkpoint() external; 27 | 28 | /** 29 | * @notice Read global token value. 30 | * @return IERC20 31 | */ 32 | function getToken() external view returns (IERC20); 33 | 34 | /** 35 | * @notice Calculate token accumulation beween timestamp range. 36 | * @dev Search the ring buffer for two checkpoint observations and diffs accumulator amount. 37 | * @param startTimestamp Account address 38 | * @param endTimestamp Transfer amount 39 | */ 40 | function getReserveAccumulatedBetween(uint32 startTimestamp, uint32 endTimestamp) 41 | external 42 | returns (uint224); 43 | 44 | /** 45 | * @notice Transfer Reserve token balance to recipient address. 46 | * @dev Creates checkpoint before token transfer. Increments withdrawAccumulator with amount. 47 | * @param recipient Account address 48 | * @param amount Transfer amount 49 | */ 50 | function withdrawTo(address recipient, uint256 amount) external; 51 | } 52 | -------------------------------------------------------------------------------- /contracts/interfaces/IPrizeDistributionSource.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | /** @title IPrizeDistributionSource 6 | * @author PoolTogether Inc Team 7 | * @notice The PrizeDistributionSource interface. 8 | */ 9 | interface IPrizeDistributionSource { 10 | ///@notice PrizeDistribution struct created every draw 11 | ///@param bitRangeSize Decimal representation of bitRangeSize 12 | ///@param matchCardinality The number of numbers to consider in the 256 bit random number. Must be > 1 and < 256/bitRangeSize. 13 | ///@param startTimestampOffset The starting time offset in seconds from which Ticket balances are calculated. 14 | ///@param endTimestampOffset The end time offset in seconds from which Ticket balances are calculated. 15 | ///@param maxPicksPerUser Maximum number of picks a user can make in this draw 16 | ///@param expiryDuration Length of time in seconds the PrizeDistribution is valid for. Relative to the Draw.timestamp. 17 | ///@param numberOfPicks Number of picks this draw has (may vary across networks according to how much the network has contributed to the Reserve) 18 | ///@param tiers Array of prize tiers percentages, expressed in fraction form with base 1e9. Ordering: index0: grandPrize, index1: runnerUp, etc. 19 | ///@param prize Total prize amount available in this draw calculator for this draw (may vary from across networks) 20 | struct PrizeDistribution { 21 | uint8 bitRangeSize; 22 | uint8 matchCardinality; 23 | uint32 startTimestampOffset; 24 | uint32 endTimestampOffset; 25 | uint32 maxPicksPerUser; 26 | uint32 expiryDuration; 27 | uint104 numberOfPicks; 28 | uint32[16] tiers; 29 | uint256 prize; 30 | } 31 | 32 | /** 33 | * @notice Gets PrizeDistribution list from array of drawIds 34 | * @param drawIds drawIds to get PrizeDistribution for 35 | * @return prizeDistributionList 36 | */ 37 | function getPrizeDistributions(uint32[] calldata drawIds) 38 | external 39 | view 40 | returns (PrizeDistribution[] memory); 41 | } 42 | -------------------------------------------------------------------------------- /contracts/test/PrizePoolHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../prize-pool/PrizePool.sol"; 6 | import "./YieldSourceStub.sol"; 7 | 8 | contract PrizePoolHarness is PrizePool { 9 | uint256 public currentTime; 10 | 11 | YieldSourceStub public stubYieldSource; 12 | 13 | constructor(address _owner, YieldSourceStub _stubYieldSource) PrizePool(_owner) { 14 | stubYieldSource = _stubYieldSource; 15 | } 16 | 17 | function mint( 18 | address _to, 19 | uint256 _amount, 20 | ITicket _controlledToken 21 | ) external { 22 | _mint(_to, _amount, _controlledToken); 23 | } 24 | 25 | function supply(uint256 mintAmount) external { 26 | _supply(mintAmount); 27 | } 28 | 29 | function redeem(uint256 redeemAmount) external { 30 | _redeem(redeemAmount); 31 | } 32 | 33 | function setCurrentTime(uint256 _nowTime) external { 34 | currentTime = _nowTime; 35 | } 36 | 37 | function _currentTime() internal view override returns (uint256) { 38 | return currentTime; 39 | } 40 | 41 | function internalCurrentTime() external view returns (uint256) { 42 | return super._currentTime(); 43 | } 44 | 45 | function _canAwardExternal(address _externalToken) internal view override returns (bool) { 46 | return stubYieldSource.canAwardExternal(_externalToken); 47 | } 48 | 49 | function _token() internal view override returns (IERC20) { 50 | return IERC20(stubYieldSource.depositToken()); 51 | } 52 | 53 | function _balance() internal override returns (uint256) { 54 | return stubYieldSource.balanceOfToken(address(this)); 55 | } 56 | 57 | function _supply(uint256 mintAmount) internal override { 58 | stubYieldSource.supplyTokenTo(mintAmount, address(this)); 59 | } 60 | 61 | function _redeem(uint256 redeemAmount) internal override returns (uint256) { 62 | return stubYieldSource.redeemToken(redeemAmount); 63 | } 64 | 65 | function setCurrentAwardBalance(uint256 amount) external { 66 | _currentAwardBalance = amount; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /contracts/test/DrawCalculatorHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../DrawCalculator.sol"; 6 | 7 | contract DrawCalculatorHarness is DrawCalculator { 8 | constructor( 9 | ITicket _ticket, 10 | IDrawBuffer _drawBuffer, 11 | IPrizeDistributionBuffer _prizeDistributionBuffer 12 | ) DrawCalculator(_ticket, _drawBuffer, _prizeDistributionBuffer) {} 13 | 14 | function calculateTierIndex( 15 | uint256 _randomNumberThisPick, 16 | uint256 _winningRandomNumber, 17 | uint256[] memory _masks 18 | ) public pure returns (uint256) { 19 | return _calculateTierIndex(_randomNumberThisPick, _winningRandomNumber, _masks); 20 | } 21 | 22 | function createBitMasks(IPrizeDistributionBuffer.PrizeDistribution calldata _prizeDistribution) 23 | public 24 | pure 25 | returns (uint256[] memory) 26 | { 27 | return _createBitMasks(_prizeDistribution); 28 | } 29 | 30 | ///@notice Calculates the expected prize fraction per prizeDistribution and prizeTierIndex 31 | ///@param _prizeDistribution prizeDistribution struct for Draw 32 | ///@param _prizeTierIndex Index of the prize tiers array to calculate 33 | ///@return returns the fraction of the total prize 34 | function calculatePrizeTierFraction( 35 | IPrizeDistributionBuffer.PrizeDistribution calldata _prizeDistribution, 36 | uint256 _prizeTierIndex 37 | ) external pure returns (uint256) { 38 | return _calculatePrizeTierFraction(_prizeDistribution, _prizeTierIndex); 39 | } 40 | 41 | function numberOfPrizesForIndex(uint8 _bitRangeSize, uint256 _prizeTierIndex) 42 | external 43 | pure 44 | returns (uint256) 45 | { 46 | return _numberOfPrizesForIndex(_bitRangeSize, _prizeTierIndex); 47 | } 48 | 49 | function calculateNumberOfUserPicks( 50 | IPrizeDistributionBuffer.PrizeDistribution memory _prizeDistribution, 51 | uint256 _normalizedUserBalance 52 | ) external pure returns (uint64) { 53 | return _calculateNumberOfUserPicks(_prizeDistribution, _normalizedUserBalance); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/test/DrawCalculatorV2Harness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../DrawCalculatorV2.sol"; 6 | 7 | contract DrawCalculatorV2Harness is DrawCalculatorV2 { 8 | constructor( 9 | ITicket _ticket, 10 | IDrawBuffer _drawBuffer, 11 | IPrizeDistributionSource _prizeDistributionSource 12 | ) DrawCalculatorV2(_ticket, _drawBuffer, _prizeDistributionSource) {} 13 | 14 | function calculateTierIndex( 15 | uint256 _randomNumberThisPick, 16 | uint256 _winningRandomNumber, 17 | uint256[] memory _masks 18 | ) public pure returns (uint256) { 19 | return _calculateTierIndex(_randomNumberThisPick, _winningRandomNumber, _masks); 20 | } 21 | 22 | function createBitMasks(IPrizeDistributionSource.PrizeDistribution calldata _prizeDistribution) 23 | public 24 | pure 25 | returns (uint256[] memory) 26 | { 27 | return _createBitMasks(_prizeDistribution); 28 | } 29 | 30 | ///@notice Calculates the expected prize fraction per prizeDistribution and prizeTierIndex 31 | ///@param _prizeDistribution prizeDistribution struct for Draw 32 | ///@param _prizeTierIndex Index of the prize tiers array to calculate 33 | ///@return returns the fraction of the total prize 34 | function calculatePrizeTierFraction( 35 | IPrizeDistributionSource.PrizeDistribution calldata _prizeDistribution, 36 | uint256 _prizeTierIndex 37 | ) external pure returns (uint256) { 38 | return _calculatePrizeTierFraction(_prizeDistribution, _prizeTierIndex); 39 | } 40 | 41 | function numberOfPrizesForIndex(uint8 _bitRangeSize, uint256 _prizeTierIndex) 42 | external 43 | pure 44 | returns (uint256) 45 | { 46 | return _numberOfPrizesForIndex(_bitRangeSize, _prizeTierIndex); 47 | } 48 | 49 | function calculateNumberOfUserPicks( 50 | IPrizeDistributionSource.PrizeDistribution memory _prizeDistribution, 51 | uint256 _normalizedUserBalance 52 | ) external pure returns (uint64) { 53 | return _calculateNumberOfUserPicks(_prizeDistribution, _normalizedUserBalance); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/test/TicketHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 6 | 7 | import "../Ticket.sol"; 8 | 9 | contract TicketHarness is Ticket { 10 | using SafeCast for uint256; 11 | 12 | constructor( 13 | string memory _name, 14 | string memory _symbol, 15 | uint8 decimals_, 16 | address _controller 17 | ) Ticket(_name, _symbol, decimals_, _controller) {} 18 | 19 | function flashLoan(address _to, uint256 _amount) external { 20 | _mint(_to, _amount); 21 | _burn(_to, _amount); 22 | } 23 | 24 | function burn(address _from, uint256 _amount) external { 25 | _burn(_from, _amount); 26 | } 27 | 28 | function mint(address _to, uint256 _amount) external { 29 | _mint(_to, _amount); 30 | } 31 | 32 | function mintTwice(address _to, uint256 _amount) external { 33 | _mint(_to, _amount); 34 | _mint(_to, _amount); 35 | } 36 | 37 | /// @dev we need to use a different function name than `transfer` 38 | /// otherwise it collides with the `transfer` function of the `ERC20` contract 39 | function transferTo( 40 | address _sender, 41 | address _recipient, 42 | uint256 _amount 43 | ) external { 44 | _transfer(_sender, _recipient, _amount); 45 | } 46 | 47 | function getBalanceTx(address _user, uint32 _target) external view returns (uint256) { 48 | TwabLib.Account storage account = userTwabs[_user]; 49 | 50 | return 51 | TwabLib.getBalanceAt(account.twabs, account.details, _target, uint32(block.timestamp)); 52 | } 53 | 54 | function getAverageBalanceTx( 55 | address _user, 56 | uint32 _startTime, 57 | uint32 _endTime 58 | ) external view returns (uint256) { 59 | TwabLib.Account storage account = userTwabs[_user]; 60 | 61 | return 62 | TwabLib.getAverageBalanceBetween( 63 | account.twabs, 64 | account.details, 65 | uint32(_startTime), 66 | uint32(_endTime), 67 | uint32(block.timestamp) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /hardhat/tsunami-tasks.js: -------------------------------------------------------------------------------- 1 | const { BigNumber, constants } = require('ethers'); 2 | const tiers = require('./prizeDistributions'); 3 | 4 | task('deposit-to') 5 | .addPositionalParam('address', 'PrizePool address') 6 | .addPositionalParam('amount', 'amount') 7 | .addPositionalParam('to', 'to') 8 | .addPositionalParam('controlledToken', 'controlledToken') 9 | .setAction(async function ({ address, amount, to, controlledToken }) { 10 | const contract = await ethers.getContractAt('YieldSourcePrizePool', address); 11 | await contract.depositTo(amount, to, controlledToken, constants.AddressZero); 12 | console.log(`Deposit To: ${address}`); 13 | }); 14 | 15 | task('push-draw') 16 | .addPositionalParam('address', 'Draw Buffer address') 17 | .addPositionalParam('drawId', 'drawId') 18 | .addPositionalParam('timestamp', 'timestamp') 19 | .addPositionalParam('winningRandomNumber', 'winningRandomNumber') 20 | .setAction(async function ({ address, drawId, timestamp, winningRandomNumber }) { 21 | const contract = await ethers.getContractAt('DrawBuffer', address); 22 | await contract.addDraw({ 23 | drawId: drawId, 24 | timestamp: timestamp, 25 | winningRandomNumber: winningRandomNumber, 26 | }); 27 | console.log(`Draw Created: ${address}`); 28 | }); 29 | 30 | task('set-draw-settings') 31 | .addPositionalParam('address', 'DrawCalculator address') 32 | .addPositionalParam('drawId', 'drawId') 33 | .addPositionalParam('bitRangeSize', 'bitRangeSize') 34 | .addPositionalParam('matchCardinality', 'matchCardinality') 35 | .addPositionalParam('numberOfPicks', 'numberOfPicks') 36 | .addPositionalParam('prize', 'prize') 37 | .setAction(async function ({ 38 | address, 39 | drawId, 40 | bitRangeSize, 41 | matchCardinality, 42 | numberOfPicks, 43 | prize, 44 | }) { 45 | const contract = await ethers.getContractAt('DrawBuffer', address); 46 | await contract.pushPrizeDistribution(drawId, { 47 | bitRangeSize: BigNumber.from(bitRangeSize), 48 | matchCardinality: BigNumber.from(matchCardinality), 49 | numberOfPicks: BigNumber.from(utils.parseEther(`${numberOfPicks}`)), 50 | prize: ethers.utils.parseEther(`${prize}`), 51 | tiers: tiers, 52 | }); 53 | console.log(`Draw Setings updated: ${address}`); 54 | }); 55 | -------------------------------------------------------------------------------- /contracts/libraries/DrawRingBufferLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./RingBufferLib.sol"; 6 | 7 | /// @title Library for creating and managing a draw ring buffer. 8 | library DrawRingBufferLib { 9 | /// @notice Draw buffer struct. 10 | struct Buffer { 11 | uint32 lastDrawId; 12 | uint32 nextIndex; 13 | uint32 cardinality; 14 | } 15 | 16 | /// @notice Helper function to know if the draw ring buffer has been initialized. 17 | /// @dev since draws start at 1 and are monotonically increased, we know we are uninitialized if nextIndex = 0 and lastDrawId = 0. 18 | /// @param _buffer The buffer to check. 19 | function isInitialized(Buffer memory _buffer) internal pure returns (bool) { 20 | return !(_buffer.nextIndex == 0 && _buffer.lastDrawId == 0); 21 | } 22 | 23 | /// @notice Push a draw to the buffer. 24 | /// @param _buffer The buffer to push to. 25 | /// @param _drawId The drawID to push. 26 | /// @return The new buffer. 27 | function push(Buffer memory _buffer, uint32 _drawId) internal pure returns (Buffer memory) { 28 | require(!isInitialized(_buffer) || _drawId == _buffer.lastDrawId + 1, "DRB/must-be-contig"); 29 | 30 | return 31 | Buffer({ 32 | lastDrawId: _drawId, 33 | nextIndex: uint32(RingBufferLib.nextIndex(_buffer.nextIndex, _buffer.cardinality)), 34 | cardinality: _buffer.cardinality 35 | }); 36 | } 37 | 38 | /// @notice Get draw ring buffer index pointer. 39 | /// @param _buffer The buffer to get the `nextIndex` from. 40 | /// @param _drawId The draw id to get the index for. 41 | /// @return The draw ring buffer index pointer. 42 | function getIndex(Buffer memory _buffer, uint32 _drawId) internal pure returns (uint32) { 43 | require(isInitialized(_buffer) && _drawId <= _buffer.lastDrawId, "DRB/future-draw"); 44 | 45 | uint32 indexOffset = _buffer.lastDrawId - _drawId; 46 | require(indexOffset < _buffer.cardinality, "DRB/expired-draw"); 47 | 48 | uint256 mostRecent = RingBufferLib.newestIndex(_buffer.nextIndex, _buffer.cardinality); 49 | 50 | return uint32(RingBufferLib.offset(uint32(mostRecent), indexOffset, _buffer.cardinality)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /contracts/interfaces/IDrawCalculator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./ITicket.sol"; 6 | import "./IDrawBuffer.sol"; 7 | import "../PrizeDistributionBuffer.sol"; 8 | import "../PrizeDistributor.sol"; 9 | 10 | /** 11 | * @title PoolTogether V4 IDrawCalculator 12 | * @author PoolTogether Inc Team 13 | * @notice The DrawCalculator interface. 14 | */ 15 | interface IDrawCalculator { 16 | struct PickPrize { 17 | bool won; 18 | uint8 tierIndex; 19 | } 20 | 21 | ///@notice Emitted when the contract is initialized 22 | event Deployed( 23 | ITicket indexed ticket, 24 | IDrawBuffer indexed drawBuffer, 25 | IPrizeDistributionBuffer indexed prizeDistributionBuffer 26 | ); 27 | 28 | ///@notice Emitted when the prizeDistributor is set/updated 29 | event PrizeDistributorSet(PrizeDistributor indexed prizeDistributor); 30 | 31 | /** 32 | * @notice Calculates the prize amount for a user for Multiple Draws. Typically called by a PrizeDistributor. 33 | * @param user User for which to calculate prize amount. 34 | * @param drawIds drawId array for which to calculate prize amounts for. 35 | * @param data The ABI encoded pick indices for all Draws. Expected to be winning picks. Pick indices must be less than the totalUserPicks. 36 | * @return List of awardable prize amounts ordered by drawId. 37 | */ 38 | function calculate( 39 | address user, 40 | uint32[] calldata drawIds, 41 | bytes calldata data 42 | ) external view returns (uint256[] memory, bytes memory); 43 | 44 | /** 45 | * @notice Read global DrawBuffer variable. 46 | * @return IDrawBuffer 47 | */ 48 | function getDrawBuffer() external view returns (IDrawBuffer); 49 | 50 | /** 51 | * @notice Read global prizeDistributionBuffer variable. 52 | * @return IPrizeDistributionBuffer 53 | */ 54 | function getPrizeDistributionBuffer() external view returns (IPrizeDistributionBuffer); 55 | 56 | /** 57 | * @notice Returns a users balances expressed as a fraction of the total supply over time. 58 | * @param user The users address 59 | * @param drawIds The drawIds to consider 60 | * @return Array of balances 61 | */ 62 | function getNormalizedBalancesForDrawIds(address user, uint32[] calldata drawIds) 63 | external 64 | view 65 | returns (uint256[] memory); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /templates/contract.hbs: -------------------------------------------------------------------------------- 1 | {{{natspec.userdoc}}} 2 | {{{natspec.devdoc}}} 3 | 4 | {{#if structs}} 5 | ## Structs 6 | {{/if}} 7 | {{#each structs}} 8 | ### `{{name}}` 9 | {{#each members}} 10 | - {{type}} {{name}} 11 | {{/each}} 12 | {{/each}} 13 | 14 | {{#if enums}} 15 | ## Enums 16 | {{/if}} 17 | {{#each enums}} 18 | ### `{{name}}` 19 | All members: {{members}} 20 | {{#each members}} 21 | - {{.}} 22 | {{/each}} 23 | {{/each}} 24 | 25 | {{#if functions}} 26 | ## Functions 27 | {{/if}} 28 | {{#functions}} 29 | {{#unless (eq visibility "internal")}} 30 | ### {{name}} 31 | ```solidity 32 | function {{name}}( 33 | {{#natspec.params}} 34 | {{#lookup ../args.types @index}}{{/lookup}} {{param}}{{#if @last}}{{else}},{{/if}} 35 | {{/natspec.params}} 36 | ) {{visibility}}{{#if outputs}} returns ({{outputs}}){{/if}} 37 | ``` 38 | {{#if natspec.userdoc}}{{natspec.userdoc}}{{/if}} 39 | {{#if natspec.devdoc}}{{natspec.devdoc}}{{/if}} 40 | {{#if natspec.params}} 41 | #### Parameters: 42 | | Name | Type | Description | 43 | | :--- | :--- | :------------------------------------------------------------------- | 44 | {{#natspec.params}} 45 | |`{{param}}` | {{#lookup ../args.types @index}}{{/lookup}} | {{ description }}{{/natspec.params}}{{/if}} 46 | {{#if natspec.returns}} 47 | #### Return Values: 48 | | Type | Description | 49 | | :------------ | :--------------------------------------------------------------------------- | 50 | {{#natspec.returns}} 51 | | {{#lookup ../outputs.types @index}}{{/lookup}} | {{param}} {{{description}}}{{/natspec.returns}}{{/if}} 52 | {{/unless}} 53 | {{/functions}} 54 | {{#if events}} 55 | ## Events 56 | {{/if}} 57 | {{#events}} 58 | ### {{name}} 59 | ```solidity 60 | event {{name}}( 61 | {{#natspec.params}} 62 | {{#lookup ../args.types @index}}{{/lookup}} {{param}}{{#if @last}}{{else}},{{/if}} 63 | {{/natspec.params}} 64 | ) 65 | ``` 66 | {{#if natspec.userdoc}}{{natspec.userdoc}}{{/if}} 67 | {{#if natspec.devdoc}}{{natspec.devdoc}}{{/if}} 68 | {{#if natspec.params}} 69 | #### Parameters: 70 | | Name | Type | Description | 71 | | :----------------------------- | :------------ | :--------------------------------------------- | 72 | {{#natspec.params}} 73 | |`{{param}}`| {{#lookup ../args.types @index}}{{/lookup}} | {{{description}}}{{/natspec.params}}{{/if}} 74 | {{/events}} 75 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-etherscan'; 2 | import '@nomiclabs/hardhat-waffle'; 3 | import 'hardhat-abi-exporter'; 4 | import 'hardhat-deploy'; 5 | import 'hardhat-deploy-ethers'; 6 | import 'hardhat-gas-reporter'; 7 | import 'hardhat-log-remover'; 8 | import 'solidity-coverage'; 9 | import 'hardhat-dependency-compiler'; 10 | import './hardhat/tsunami-tasks.js'; 11 | import { HardhatUserConfig } from 'hardhat/config'; 12 | import networks from './hardhat.network'; 13 | 14 | const optimizerEnabled = !process.env.OPTIMIZER_DISABLED; 15 | 16 | const config: HardhatUserConfig = { 17 | abiExporter: { 18 | path: './abis', 19 | clear: true, 20 | flat: true, 21 | }, 22 | etherscan: { 23 | apiKey: process.env.ETHERSCAN_API_KEY, 24 | }, 25 | gasReporter: { 26 | currency: 'USD', 27 | gasPrice: 100, 28 | enabled: process.env.REPORT_GAS ? true : false, 29 | }, 30 | mocha: { 31 | timeout: 30000, 32 | }, 33 | namedAccounts: { 34 | deployer: { 35 | default: 0, 36 | }, 37 | testnetCDai: { 38 | // 1: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 39 | 4: '0x6D7F0754FFeb405d23C51CE938289d4835bE3b14', 40 | 42: '0xF0d0EB522cfa50B716B3b1604C4F0fA6f04376AD', 41 | }, 42 | }, 43 | networks, 44 | solidity: { 45 | compilers: [ 46 | { 47 | version: '0.8.6', 48 | settings: { 49 | optimizer: { 50 | enabled: optimizerEnabled, 51 | runs: 2000, 52 | }, 53 | evmVersion: 'berlin', 54 | }, 55 | }, 56 | { 57 | version: '0.7.6', 58 | settings: { 59 | optimizer: { 60 | enabled: optimizerEnabled, 61 | runs: 2000, 62 | }, 63 | evmVersion: 'berlin', 64 | }, 65 | }, 66 | ], 67 | }, 68 | external: { 69 | contracts: [ 70 | { 71 | artifacts: 'node_modules/@pooltogether/pooltogether-rng-contracts/artifacts', 72 | }, 73 | ], 74 | }, 75 | dependencyCompiler: { 76 | paths: [ 77 | '@pooltogether/yield-source-interface/contracts/test/MockYieldSource.sol', 78 | ], 79 | }, 80 | }; 81 | 82 | export default config; 83 | -------------------------------------------------------------------------------- /test/libraries/ExtendedSafeCast.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { BigNumber, Contract, ContractFactory } from 'ethers'; 3 | import { ethers } from 'hardhat'; 4 | 5 | const { utils } = ethers; 6 | const { parseEther: toWei } = utils; 7 | 8 | describe('ExtendedSafeCastLib', () => { 9 | let ExtendedSafeCastLib: Contract; 10 | let ExtendedSafeCastLibFactory: ContractFactory; 11 | 12 | before(async () => { 13 | ExtendedSafeCastLibFactory = await ethers.getContractFactory('ExtendedSafeCastLibHarness'); 14 | ExtendedSafeCastLib = await ExtendedSafeCastLibFactory.deploy(); 15 | }); 16 | 17 | describe('toUint104()', () => { 18 | it('should return uint256 downcasted to uint104', async () => { 19 | const value = toWei('1'); 20 | 21 | expect(await ExtendedSafeCastLib.toUint104(value)).to.equal(value); 22 | }); 23 | it('should fail to return value if value passed does not fit in 104 bits', async () => { 24 | const value = BigNumber.from(2).pow(104); 25 | 26 | await expect(ExtendedSafeCastLib.toUint104(value)).to.be.revertedWith( 27 | "SafeCast: value doesn't fit in 104 bits", 28 | ); 29 | }); 30 | }); 31 | 32 | describe('toUint208()', () => { 33 | it('should return uint256 downcasted to uint208', async () => { 34 | const value = toWei('1'); 35 | 36 | expect(await ExtendedSafeCastLib.toUint208(value)).to.equal(value); 37 | }); 38 | it('should fail to return value if value passed does not fit in 208 bits', async () => { 39 | const value = BigNumber.from(2).pow(209); 40 | 41 | await expect(ExtendedSafeCastLib.toUint208(value)).to.be.revertedWith( 42 | "SafeCast: value doesn't fit in 208 bits", 43 | ); 44 | }); 45 | }); 46 | 47 | describe('toUint224()', () => { 48 | it('should return uint256 downcasted to uint224', async () => { 49 | const value = toWei('1'); 50 | 51 | expect(await ExtendedSafeCastLib.toUint224(value)).to.equal(value); 52 | }); 53 | 54 | it('should fail to return value if value passed does not fit in 208 bits', async () => { 55 | const value = BigNumber.from(2).pow(224); 56 | 57 | await expect(ExtendedSafeCastLib.toUint224(value)).to.be.revertedWith( 58 | "SafeCast: value doesn't fit in 224 bits", 59 | ); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /contracts/libraries/ExtendedSafeCastLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | /** 6 | * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow 7 | * checks. 8 | * 9 | * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can 10 | * easily result in undesired exploitation or bugs, since developers usually 11 | * assume that overflows raise errors. `SafeCast` restores this intuition by 12 | * reverting the transaction when such an operation overflows. 13 | * 14 | * Using this library instead of the unchecked operations eliminates an entire 15 | * class of bugs, so it's recommended to use it always. 16 | * 17 | * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing 18 | * all math on `uint256` and `int256` and then downcasting. 19 | */ 20 | library ExtendedSafeCastLib { 21 | 22 | /** 23 | * @dev Returns the downcasted uint104 from uint256, reverting on 24 | * overflow (when the input is greater than largest uint104). 25 | * 26 | * Counterpart to Solidity's `uint104` operator. 27 | * 28 | * Requirements: 29 | * 30 | * - input must fit into 104 bits 31 | */ 32 | function toUint104(uint256 _value) internal pure returns (uint104) { 33 | require(_value <= type(uint104).max, "SafeCast: value doesn't fit in 104 bits"); 34 | return uint104(_value); 35 | } 36 | 37 | /** 38 | * @dev Returns the downcasted uint208 from uint256, reverting on 39 | * overflow (when the input is greater than largest uint208). 40 | * 41 | * Counterpart to Solidity's `uint208` operator. 42 | * 43 | * Requirements: 44 | * 45 | * - input must fit into 208 bits 46 | */ 47 | function toUint208(uint256 _value) internal pure returns (uint208) { 48 | require(_value <= type(uint208).max, "SafeCast: value doesn't fit in 208 bits"); 49 | return uint208(_value); 50 | } 51 | 52 | /** 53 | * @dev Returns the downcasted uint224 from uint256, reverting on 54 | * overflow (when the input is greater than largest uint224). 55 | * 56 | * Counterpart to Solidity's `uint224` operator. 57 | * 58 | * Requirements: 59 | * 60 | * - input must fit into 224 bits 61 | */ 62 | function toUint224(uint256 _value) internal pure returns (uint224) { 63 | require(_value <= type(uint224).max, "SafeCast: value doesn't fit in 224 bits"); 64 | return uint224(_value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/features/ticket.test.js: -------------------------------------------------------------------------------- 1 | const { PoolEnv } = require('./support/PoolEnv'); 2 | const ethers = require('ethers'); 3 | 4 | const toWei = (val) => ethers.utils.parseEther('' + val); 5 | 6 | describe('Tickets', () => { 7 | let env; 8 | 9 | beforeEach(async () => { 10 | env = new PoolEnv(); 11 | await env.ready(); 12 | }); 13 | 14 | it('should be possible to purchase tickets', async () => { 15 | await env.buyTickets({ user: 1, tickets: 100 }); 16 | await env.buyTickets({ user: 2, tickets: 50 }); 17 | await env.expectUserToHaveTickets({ user: 1, tickets: 100 }); 18 | await env.expectUserToHaveTickets({ user: 2, tickets: 50 }); 19 | }); 20 | 21 | it('should be possible to withdraw tickets', async () => { 22 | await env.buyTickets({ user: 1, tickets: 100 }); 23 | 24 | // they deposited all of their tokens 25 | await env.expectUserToHaveTokens({ user: 1, tokens: 0 }); 26 | await env.withdraw({ user: 1, tickets: 100 }); 27 | await env.expectUserToHaveTokens({ user: 1, tokens: 100 }); 28 | }); 29 | 30 | // NOT a COMPLETE test. Needs to be fixed - out of scope for this PR. 31 | it.skip('should allow a user to pull their prizes', async () => { 32 | await env.buyTickets({ user: 1, tickets: 100 }); 33 | await env.buyTicketsForPrizeDistributor({ 34 | user: 1, 35 | tickets: 100, 36 | prizeDistributor: (await env.prizeDistributor()).address, 37 | }); 38 | 39 | const wallet = await env.wallet(1); 40 | 41 | const winningNumber = ethers.utils.solidityKeccak256(['address'], [wallet.address]); 42 | const winningRandomNumber = ethers.utils.solidityKeccak256( 43 | ['bytes32', 'uint256'], 44 | [winningNumber, 1], 45 | ); 46 | 47 | await env.poolAccrues({ tickets: 10 }); 48 | await env.draw({ randomNumber: winningRandomNumber }); 49 | 50 | await env.pushPrizeDistribution({ 51 | drawId: 1, 52 | bitRangeSize: ethers.BigNumber.from(4), 53 | matchCardinality: ethers.BigNumber.from(5), 54 | numberOfPicks: toWei('1'), 55 | tiers: [ethers.utils.parseUnits('0.8', 9), ethers.utils.parseUnits('0.2', 9)], 56 | prize: toWei('10'), 57 | startTimestampOffset: 5, 58 | endTimestampOffset: 10, 59 | maxPicksPerUser: 1000, 60 | }); 61 | 62 | await env.claim({ user: 1, drawId: 1, picks: [1], prize: 100 }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /scripts/verify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const chalk = require('chalk') 3 | const util = require('util') 4 | const exec = util.promisify(require('child_process').exec) 5 | const hardhat = require('hardhat') 6 | 7 | const info = (msg) => console.log(chalk.dim(msg)) 8 | const success = (msg) => console.log(chalk.green(msg)) 9 | const error = (msg) => console.error(chalk.red(msg)) 10 | 11 | const getContract = async (name) => { 12 | const { deployments } = hardhat 13 | const signers = await hardhat.ethers.getSigners() 14 | return hardhat.ethers.getContractAt(name, (await deployments.get(name)).address, signers[0]) 15 | } 16 | 17 | const verifyAddress = async (address, name) => { 18 | const network = hardhat.network.name 19 | const config = isBinance() ? '--config hardhat.config.bsc.js' : '' 20 | try { 21 | await exec(`hardhat ${config} verify --network ${network} ${address}`) 22 | } catch (e) { 23 | if (/Contract source code already verified/.test(e.message)) { 24 | info(`${name} already verified`) 25 | } else { 26 | error(e.message) 27 | console.error(e) 28 | } 29 | } 30 | } 31 | 32 | const verifyProxyFactory = async (name) => { 33 | const proxyFactory = await getContract(name) 34 | const instanceAddress = await proxyFactory.instance() 35 | info(`Verifying ${name} instance at ${instanceAddress}...`) 36 | await verifyAddress(instanceAddress, name) 37 | success(`Verified!`) 38 | } 39 | 40 | function isBinance() { 41 | const network = hardhat.network.name 42 | return /bsc/.test(network); 43 | } 44 | 45 | function etherscanApiKey() { 46 | if (isBinance()) { 47 | return process.env.BSCSCAN_API_KEY 48 | } else { 49 | return process.env.ETHERSCAN_API_KEY 50 | } 51 | } 52 | 53 | async function run() { 54 | const network = hardhat.network.name 55 | 56 | info(`Verifying top-level contracts...`) 57 | const { stdout, stderr } = await exec( 58 | `hardhat --network ${network} etherscan-verify --solc-input --api-key ${etherscanApiKey()}` 59 | ) 60 | console.log(chalk.yellow(stdout)) 61 | console.log(chalk.red(stderr)) 62 | info(`Done top-level contracts`) 63 | 64 | info(`Verifying proxy factory instances...`) 65 | 66 | await verifyProxyFactory('CompoundPrizePoolProxyFactory') 67 | await verifyProxyFactory('ControlledTokenProxyFactory') 68 | await verifyProxyFactory('MultipleWinnersProxyFactory') 69 | await verifyProxyFactory('StakePrizePoolProxyFactory') 70 | await verifyProxyFactory('TicketProxyFactory') 71 | await verifyProxyFactory('TokenFaucetProxyFactory') 72 | await verifyProxyFactory('YieldSourcePrizePoolProxyFactory') 73 | 74 | success('Done!') 75 | } 76 | 77 | run() 78 | -------------------------------------------------------------------------------- /contracts/libraries/RingBufferLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | library RingBufferLib { 6 | /** 7 | * @notice Returns wrapped TWAB index. 8 | * @dev In order to navigate the TWAB circular buffer, we need to use the modulo operator. 9 | * @dev For example, if `_index` is equal to 32 and the TWAB circular buffer is of `_cardinality` 32, 10 | * it will return 0 and will point to the first element of the array. 11 | * @param _index Index used to navigate through the TWAB circular buffer. 12 | * @param _cardinality TWAB buffer cardinality. 13 | * @return TWAB index. 14 | */ 15 | function wrap(uint256 _index, uint256 _cardinality) internal pure returns (uint256) { 16 | return _index % _cardinality; 17 | } 18 | 19 | /** 20 | * @notice Computes the negative offset from the given index, wrapped by the cardinality. 21 | * @dev We add `_cardinality` to `_index` to be able to offset even if `_amount` is superior to `_cardinality`. 22 | * @param _index The index from which to offset 23 | * @param _amount The number of indices to offset. This is subtracted from the given index. 24 | * @param _cardinality The number of elements in the ring buffer 25 | * @return Offsetted index. 26 | */ 27 | function offset( 28 | uint256 _index, 29 | uint256 _amount, 30 | uint256 _cardinality 31 | ) internal pure returns (uint256) { 32 | return wrap(_index + _cardinality - _amount, _cardinality); 33 | } 34 | 35 | /// @notice Returns the index of the last recorded TWAB 36 | /// @param _nextIndex The next available twab index. This will be recorded to next. 37 | /// @param _cardinality The cardinality of the TWAB history. 38 | /// @return The index of the last recorded TWAB 39 | function newestIndex(uint256 _nextIndex, uint256 _cardinality) 40 | internal 41 | pure 42 | returns (uint256) 43 | { 44 | if (_cardinality == 0) { 45 | return 0; 46 | } 47 | 48 | return wrap(_nextIndex + _cardinality - 1, _cardinality); 49 | } 50 | 51 | /// @notice Computes the ring buffer index that follows the given one, wrapped by cardinality 52 | /// @param _index The index to increment 53 | /// @param _cardinality The number of elements in the Ring Buffer 54 | /// @return The next index relative to the given index. Will wrap around to 0 if the next index == cardinality 55 | function nextIndex(uint256 _index, uint256 _cardinality) 56 | internal 57 | pure 58 | returns (uint256) 59 | { 60 | return wrap(_index + 1, _cardinality); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /hardhat.network.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config'; 2 | 3 | const alchemyUrl = process.env.ALCHEMY_URL; 4 | const infuraApiKey = process.env.INFURA_API_KEY; 5 | const mnemonic = process.env.HDWALLET_MNEMONIC; 6 | 7 | const networks: HardhatUserConfig['networks'] = { 8 | coverage: { 9 | url: 'http://127.0.0.1:8555', 10 | blockGasLimit: 200000000, 11 | allowUnlimitedContractSize: true, 12 | }, 13 | localhost: { 14 | chainId: 1, 15 | url: 'http://127.0.0.1:8545', 16 | allowUnlimitedContractSize: true, 17 | }, 18 | }; 19 | 20 | if (alchemyUrl && process.env.FORK_ENABLED && mnemonic) { 21 | networks.hardhat = { 22 | chainId: 1, 23 | allowUnlimitedContractSize: true, 24 | gas: 12000000, 25 | blockGasLimit: 0x1fffffffffffff, 26 | forking: { 27 | url: alchemyUrl, 28 | }, 29 | accounts: { 30 | mnemonic, 31 | }, 32 | }; 33 | } else { 34 | networks.hardhat = { 35 | allowUnlimitedContractSize: true, 36 | gas: 12000000, 37 | initialBaseFeePerGas: 0, // temporary fix, remove once we bump version: https://github.com/sc-forks/solidity-coverage/issues/652#issuecomment-896330136 38 | blockGasLimit: 0x1fffffffffffff, 39 | }; 40 | } 41 | 42 | if (mnemonic) { 43 | networks.xdai = { 44 | chainId: 100, 45 | url: 'https://rpc.xdaichain.com/', 46 | accounts: { 47 | mnemonic, 48 | }, 49 | }; 50 | networks.poaSokol = { 51 | chainId: 77, 52 | url: 'https://sokol.poa.network', 53 | accounts: { 54 | mnemonic, 55 | }, 56 | }; 57 | networks.matic = { 58 | chainId: 137, 59 | url: 'https://rpc-mainnet.maticvigil.com', 60 | accounts: { 61 | mnemonic, 62 | }, 63 | }; 64 | networks.mumbai = { 65 | chainId: 80001, 66 | url: 'https://rpc-mumbai.maticvigil.com', 67 | accounts: { 68 | mnemonic, 69 | }, 70 | }; 71 | } 72 | 73 | if (infuraApiKey && mnemonic) { 74 | networks.kovan = { 75 | url: `https://kovan.infura.io/v3/${infuraApiKey}`, 76 | accounts: { 77 | mnemonic, 78 | }, 79 | }; 80 | 81 | networks.ropsten = { 82 | url: `https://ropsten.infura.io/v3/${infuraApiKey}`, 83 | accounts: { 84 | mnemonic, 85 | }, 86 | }; 87 | 88 | networks.rinkeby = { 89 | url: `https://rinkeby.infura.io/v3/${infuraApiKey}`, 90 | accounts: { 91 | mnemonic, 92 | }, 93 | }; 94 | 95 | networks.mainnet = { 96 | url: alchemyUrl, 97 | accounts: { 98 | mnemonic, 99 | }, 100 | }; 101 | } else { 102 | console.warn('No infura or hdwallet available for testnets'); 103 | } 104 | 105 | export default networks; 106 | -------------------------------------------------------------------------------- /test/helpers/delegateSignature.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import { ethers, Contract } from 'ethers'; 3 | 4 | const domainSchema = [ 5 | { name: 'name', type: 'string' }, 6 | { name: 'version', type: 'string' }, 7 | { name: 'chainId', type: 'uint256' }, 8 | { name: 'verifyingContract', type: 'address' }, 9 | ]; 10 | 11 | const permitSchema = [ 12 | { name: 'user', type: 'address' }, 13 | { name: 'delegate', type: 'address' }, 14 | { name: 'nonce', type: 'uint256' }, 15 | { name: 'deadline', type: 'uint256' }, 16 | ]; 17 | 18 | export const signDelegateMessage = async (signer: any, domain: any, message: any) => { 19 | let myAddr = signer.address; 20 | 21 | if (myAddr.toLowerCase() !== message.user.toLowerCase()) { 22 | throw `signDelegate: address of signer does not match user address in message`; 23 | } 24 | 25 | if (message.nonce === undefined) { 26 | let tokenAbi = ['function nonces(address user) view returns (uint)']; 27 | 28 | let tokenContract = new Contract(domain.verifyingContract, tokenAbi, signer); 29 | 30 | let nonce = await tokenContract.nonces(myAddr); 31 | 32 | message = { ...message, nonce: nonce.toString() }; 33 | } 34 | 35 | let typedData = { 36 | types: { 37 | EIP712Domain: domainSchema, 38 | Delegate: permitSchema, 39 | }, 40 | primaryType: 'Delegate', 41 | domain, 42 | message, 43 | }; 44 | 45 | let sig; 46 | 47 | if (signer && signer.provider) { 48 | try { 49 | sig = await signer.provider.send('eth_signTypedData', [myAddr, typedData]); 50 | } catch (e: any) { 51 | if (/is not supported/.test(e.message)) { 52 | sig = await signer.provider.send('eth_signTypedData_v4', [myAddr, typedData]); 53 | } 54 | } 55 | } 56 | 57 | return { domain, message, sig }; 58 | } 59 | 60 | 61 | 62 | type Delegate = { 63 | ticket: Contract, 64 | userWallet: SignerWithAddress; 65 | delegate: string; 66 | }; 67 | 68 | export async function delegateSignature({ 69 | ticket, 70 | userWallet, 71 | delegate 72 | }: Delegate): Promise { 73 | const nonce = (await ticket.nonces(userWallet.address)).toNumber() 74 | const chainId = (await ticket.provider.getNetwork()).chainId 75 | const deadline = (await ticket.provider.getBlock('latest')).timestamp + 100 76 | 77 | let delegateSig = await signDelegateMessage( 78 | userWallet, 79 | { 80 | name: 'PoolTogether ControlledToken', 81 | version: '1', 82 | chainId, 83 | verifyingContract: ticket.address, 84 | }, 85 | { 86 | user: userWallet.address, 87 | delegate, 88 | nonce, 89 | deadline, 90 | }, 91 | ); 92 | 93 | const sig = ethers.utils.splitSignature(delegateSig.sig); 94 | 95 | return { 96 | user: userWallet.address, 97 | delegate, 98 | nonce, 99 | deadline, 100 | ...sig 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /contracts/prize-pool/StakePrizePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "./PrizePool.sol"; 8 | 9 | /** 10 | * @title PoolTogether V4 StakePrizePool 11 | * @author PoolTogether Inc Team 12 | * @notice The Stake Prize Pool is a prize pool in which users can deposit an ERC20 token. 13 | * These tokens are simply held by the Stake Prize Pool and become eligible for prizes. 14 | * Prizes are added manually by the Stake Prize Pool owner and are distributed to users at the end of the prize period. 15 | */ 16 | contract StakePrizePool is PrizePool { 17 | /// @notice Address of the stake token. 18 | IERC20 private stakeToken; 19 | 20 | /// @dev Emitted when stake prize pool is deployed. 21 | /// @param stakeToken Address of the stake token. 22 | event Deployed(IERC20 indexed stakeToken); 23 | 24 | /// @notice Deploy the Stake Prize Pool 25 | /// @param _owner Address of the Stake Prize Pool owner 26 | /// @param _stakeToken Address of the stake token 27 | constructor(address _owner, IERC20 _stakeToken) PrizePool(_owner) { 28 | require(address(_stakeToken) != address(0), "StakePrizePool/stake-token-not-zero-address"); 29 | stakeToken = _stakeToken; 30 | 31 | emit Deployed(_stakeToken); 32 | } 33 | 34 | /// @notice Determines whether the passed token can be transferred out as an external award. 35 | /// @dev Different yield sources will hold the deposits as another kind of token: such a Compound's cToken. The 36 | /// prize strategy should not be allowed to move those tokens. 37 | /// @param _externalToken The address of the token to check 38 | /// @return True if the token may be awarded, false otherwise 39 | function _canAwardExternal(address _externalToken) internal view override returns (bool) { 40 | return address(stakeToken) != _externalToken; 41 | } 42 | 43 | /// @notice Returns the total balance (in asset tokens). This includes the deposits and interest. 44 | /// @return The underlying balance of asset tokens 45 | function _balance() internal view override returns (uint256) { 46 | return stakeToken.balanceOf(address(this)); 47 | } 48 | 49 | /// @notice Returns the address of the ERC20 asset token used for deposits. 50 | /// @return Address of the ERC20 asset token. 51 | function _token() internal view override returns (IERC20) { 52 | return stakeToken; 53 | } 54 | 55 | /// @notice Supplies asset tokens to the yield source. 56 | /// @param _mintAmount The amount of asset tokens to be supplied 57 | function _supply(uint256 _mintAmount) internal pure override { 58 | // no-op because nothing else needs to be done 59 | } 60 | 61 | /// @notice Redeems asset tokens from the yield source. 62 | /// @param _redeemAmount The amount of yield-bearing tokens to be redeemed 63 | /// @return The actual amount of tokens that were redeemed. 64 | function _redeem(uint256 _redeemAmount) internal pure override returns (uint256) { 65 | return _redeemAmount; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/prize-strategy/PrizeSplitStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./PrizeSplit.sol"; 6 | import "../interfaces/IStrategy.sol"; 7 | import "../interfaces/IPrizePool.sol"; 8 | 9 | /** 10 | * @title PoolTogether V4 PrizeSplitStrategy 11 | * @author PoolTogether Inc Team 12 | * @notice Captures PrizePool interest for PrizeReserve and additional PrizeSplit recipients. 13 | The PrizeSplitStrategy will have at minimum a single PrizeSplit with 100% of the captured 14 | interest transfered to the PrizeReserve. Additional PrizeSplits can be added, depending on 15 | the deployers requirements (i.e. percentage to charity). In contrast to previous PoolTogether 16 | iterations, interest can be captured independent of a new Draw. Ideally (to save gas) interest 17 | is only captured when also distributing the captured prize(s) to applicable Prize Distributor(s). 18 | */ 19 | contract PrizeSplitStrategy is PrizeSplit, IStrategy { 20 | /** 21 | * @notice PrizePool address 22 | */ 23 | IPrizePool internal immutable prizePool; 24 | 25 | /** 26 | * @notice Deployed Event 27 | * @param owner Contract owner 28 | * @param prizePool Linked PrizePool contract 29 | */ 30 | event Deployed(address indexed owner, IPrizePool prizePool); 31 | 32 | /* ============ Constructor ============ */ 33 | 34 | /** 35 | * @notice Deploy the PrizeSplitStrategy smart contract. 36 | * @param _owner Owner address 37 | * @param _prizePool PrizePool address 38 | */ 39 | constructor(address _owner, IPrizePool _prizePool) Ownable(_owner) { 40 | require( 41 | address(_prizePool) != address(0), 42 | "PrizeSplitStrategy/prize-pool-not-zero-address" 43 | ); 44 | prizePool = _prizePool; 45 | emit Deployed(_owner, _prizePool); 46 | } 47 | 48 | /* ============ External Functions ============ */ 49 | 50 | /// @inheritdoc IStrategy 51 | function distribute() external override returns (uint256) { 52 | uint256 prize = prizePool.captureAwardBalance(); 53 | 54 | if (prize == 0) return 0; 55 | 56 | uint256 prizeRemaining = _distributePrizeSplits(prize); 57 | 58 | emit Distributed(prize - prizeRemaining); 59 | 60 | return prize; 61 | } 62 | 63 | /// @inheritdoc IPrizeSplit 64 | function getPrizePool() external view override returns (IPrizePool) { 65 | return prizePool; 66 | } 67 | 68 | /* ============ Internal Functions ============ */ 69 | 70 | /** 71 | * @notice Award ticket tokens to prize split recipient. 72 | * @dev Award ticket tokens to prize split recipient via the linked PrizePool contract. 73 | * @param _to Recipient of minted tokens. 74 | * @param _amount Amount of minted tokens. 75 | */ 76 | function _awardPrizeSplitAmount(address _to, uint256 _amount) internal override { 77 | IControlledToken _ticket = prizePool.getTicket(); 78 | prizePool.award(_to, _amount); 79 | emit PrizeSplitAwarded(_to, _amount, _ticket); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /contracts/interfaces/IDrawBuffer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../interfaces/IDrawBeacon.sol"; 6 | 7 | /** @title IDrawBuffer 8 | * @author PoolTogether Inc Team 9 | * @notice The DrawBuffer interface. 10 | */ 11 | interface IDrawBuffer { 12 | /** 13 | * @notice Emit when a new draw has been created. 14 | * @param drawId Draw id 15 | * @param draw The Draw struct 16 | */ 17 | event DrawSet(uint32 indexed drawId, IDrawBeacon.Draw draw); 18 | 19 | /** 20 | * @notice Read a ring buffer cardinality 21 | * @return Ring buffer cardinality 22 | */ 23 | function getBufferCardinality() external view returns (uint32); 24 | 25 | /** 26 | * @notice Read a Draw from the draws ring buffer. 27 | * @dev Read a Draw using the Draw.drawId to calculate position in the draws ring buffer. 28 | * @param drawId Draw.drawId 29 | * @return IDrawBeacon.Draw 30 | */ 31 | function getDraw(uint32 drawId) external view returns (IDrawBeacon.Draw memory); 32 | 33 | /** 34 | * @notice Read multiple Draws from the draws ring buffer. 35 | * @dev Read multiple Draws using each drawId to calculate position in the draws ring buffer. 36 | * @param drawIds Array of drawIds 37 | * @return IDrawBeacon.Draw[] 38 | */ 39 | function getDraws(uint32[] calldata drawIds) external view returns (IDrawBeacon.Draw[] memory); 40 | 41 | /** 42 | * @notice Gets the number of Draws held in the draw ring buffer. 43 | * @dev If no Draws have been pushed, it will return 0. 44 | * @dev If the ring buffer is full, it will return the cardinality. 45 | * @dev Otherwise, it will return the NewestDraw index + 1. 46 | * @return Number of Draws held in the draw ring buffer. 47 | */ 48 | function getDrawCount() external view returns (uint32); 49 | 50 | /** 51 | * @notice Read newest Draw from draws ring buffer. 52 | * @dev Uses the nextDrawIndex to calculate the most recently added Draw. 53 | * @return IDrawBeacon.Draw 54 | */ 55 | function getNewestDraw() external view returns (IDrawBeacon.Draw memory); 56 | 57 | /** 58 | * @notice Read oldest Draw from draws ring buffer. 59 | * @dev Finds the oldest Draw by comparing and/or diffing totalDraws with the cardinality. 60 | * @return IDrawBeacon.Draw 61 | */ 62 | function getOldestDraw() external view returns (IDrawBeacon.Draw memory); 63 | 64 | /** 65 | * @notice Push Draw onto draws ring buffer history. 66 | * @dev Push new draw onto draws history via authorized manager or owner. 67 | * @param draw IDrawBeacon.Draw 68 | * @return Draw.drawId 69 | */ 70 | function pushDraw(IDrawBeacon.Draw calldata draw) external returns (uint32); 71 | 72 | /** 73 | * @notice Set existing Draw in draws ring buffer with new parameters. 74 | * @dev Updating a Draw should be used sparingly and only in the event an incorrect Draw parameter has been stored. 75 | * @param newDraw IDrawBeacon.Draw 76 | * @return Draw.drawId 77 | */ 78 | function setDraw(IDrawBeacon.Draw calldata newDraw) external returns (uint32); 79 | } 80 | -------------------------------------------------------------------------------- /contracts/libraries/OverflowSafeComparatorLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | /// @title OverflowSafeComparatorLib library to share comparator functions between contracts 6 | /// @dev Code taken from Uniswap V3 Oracle.sol: https://github.com/Uniswap/v3-core/blob/3e88af408132fc957e3e406f65a0ce2b1ca06c3d/contracts/libraries/Oracle.sol 7 | /// @author PoolTogether Inc. 8 | library OverflowSafeComparatorLib { 9 | /// @notice 32-bit timestamps comparator. 10 | /// @dev safe for 0 or 1 overflows, `_a` and `_b` must be chronologically before or equal to time. 11 | /// @param _a A comparison timestamp from which to determine the relative position of `_timestamp`. 12 | /// @param _b Timestamp to compare against `_a`. 13 | /// @param _timestamp A timestamp truncated to 32 bits. 14 | /// @return bool Whether `_a` is chronologically < `_b`. 15 | function lt( 16 | uint32 _a, 17 | uint32 _b, 18 | uint32 _timestamp 19 | ) internal pure returns (bool) { 20 | // No need to adjust if there hasn't been an overflow 21 | if (_a <= _timestamp && _b <= _timestamp) return _a < _b; 22 | 23 | uint256 aAdjusted = _a > _timestamp ? _a : _a + 2**32; 24 | uint256 bAdjusted = _b > _timestamp ? _b : _b + 2**32; 25 | 26 | return aAdjusted < bAdjusted; 27 | } 28 | 29 | /// @notice 32-bit timestamps comparator. 30 | /// @dev safe for 0 or 1 overflows, `_a` and `_b` must be chronologically before or equal to time. 31 | /// @param _a A comparison timestamp from which to determine the relative position of `_timestamp`. 32 | /// @param _b Timestamp to compare against `_a`. 33 | /// @param _timestamp A timestamp truncated to 32 bits. 34 | /// @return bool Whether `_a` is chronologically <= `_b`. 35 | function lte( 36 | uint32 _a, 37 | uint32 _b, 38 | uint32 _timestamp 39 | ) internal pure returns (bool) { 40 | 41 | // No need to adjust if there hasn't been an overflow 42 | if (_a <= _timestamp && _b <= _timestamp) return _a <= _b; 43 | 44 | uint256 aAdjusted = _a > _timestamp ? _a : _a + 2**32; 45 | uint256 bAdjusted = _b > _timestamp ? _b : _b + 2**32; 46 | 47 | return aAdjusted <= bAdjusted; 48 | } 49 | 50 | /// @notice 32-bit timestamp subtractor 51 | /// @dev safe for 0 or 1 overflows, where `_a` and `_b` must be chronologically before or equal to time 52 | /// @param _a The subtraction left operand 53 | /// @param _b The subtraction right operand 54 | /// @param _timestamp The current time. Expected to be chronologically after both. 55 | /// @return The difference between a and b, adjusted for overflow 56 | function checkedSub( 57 | uint32 _a, 58 | uint32 _b, 59 | uint32 _timestamp 60 | ) internal pure returns (uint32) { 61 | // No need to adjust if there hasn't been an overflow 62 | 63 | if (_a <= _timestamp && _b <= _timestamp) return _a - _b; 64 | 65 | uint256 aAdjusted = _a > _timestamp ? _a : _a + 2**32; 66 | uint256 bAdjusted = _b > _timestamp ? _b : _b + 2**32; 67 | 68 | return uint32(aAdjusted - bAdjusted); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pooltogether/v4-core", 3 | "version": "1.3.1", 4 | "description": "PoolTogether V4 Core Smart Contracts", 5 | "main": "index.js", 6 | "license": "GPL-3.0", 7 | "scripts": { 8 | "clean": "rm -rf cache/ artifacts/", 9 | "docs": "solidity-docgen --solc-module solc-0.8 -i contracts -o docs -t templates", 10 | "compile": "mkdir -p abis && hardhat --show-stack-traces --max-memory 8192 compile", 11 | "coverage": "HIDE_DEPLOY_LOG=true OPTIMIZER_DISABLED=true hardhat coverage", 12 | "coverage:file": "HIDE_DEPLOY_LOG=true OPTIMIZER_DISABLED=true hardhat coverage --testfiles", 13 | "deploy": "hardhat deploy --write true --network", 14 | "verify": "hardhat etherscan-verify --license MIT --solc-input --network", 15 | "etherscan-verify": "hardhat etherscan:verify --network", 16 | "format": "prettier --config .prettierrc --write \"**/*.*.{ts,js}\" \"contracts/**/*.sol\"", 17 | "format:file": "prettier --config .prettierrc --write", 18 | "hint": "solhint \"contracts/**/*.sol\"", 19 | "start-fork": "FORK_ENABLED=true hardhat node --no-reset --no-deploy", 20 | "impersonate-accounts": "hardhat --network localhost fork:impersonate-accounts ", 21 | "distribute-ether": "hardhat --network localhost fork:distribute-ether-from-binance", 22 | "create-prize-pool": "hardhat --network localhost fork:create-aave-prize-pool", 23 | "remove-logs": "yarn run hardhat remove-logs", 24 | "run-fork": "yarn impersonate-accounts && yarn distribute-ether && yarn create-prize-pool", 25 | "test": "HIDE_DEPLOY_LOG=true hardhat test", 26 | "gas": "REPORT_GAS=true HIDE_DEPLOY_LOG=true hardhat test", 27 | "parallel-test": "mocha --require hardhat/register --recursive --parallel --exit --extension ts", 28 | "prepack": "yarn compile" 29 | }, 30 | "dependencies": { 31 | "@openzeppelin/contracts": "4.3.1", 32 | "@pooltogether/fixed-point": "1.0.0", 33 | "@pooltogether/owner-manager-contracts": "1.1.0", 34 | "@pooltogether/pooltogether-rng-contracts": "1.5.2", 35 | "@pooltogether/yield-source-interface": "1.4.0", 36 | "deploy-eip-1820": "1.0.0" 37 | }, 38 | "devDependencies": { 39 | "@nomiclabs/hardhat-ethers": "2.0.2", 40 | "@nomiclabs/hardhat-etherscan": "2.1.1", 41 | "@nomiclabs/hardhat-waffle": "2.0.1", 42 | "@openzeppelin/hardhat-upgrades": "1.6.0", 43 | "@pooltogether/pooltogether-proxy-factory-package": "1.0.1", 44 | "@pooltogether/uniform-random-number": "1.0.0-beta.2", 45 | "@types/chai": "4.2.15", 46 | "@types/debug": "4.1.5", 47 | "@types/mocha": "8.2.1", 48 | "@types/node": "14.14.32", 49 | "@types/react-table": "^7.7.3", 50 | "chai": "4.3.4", 51 | "chalk": "4.1.0", 52 | "debug": "4.3.1", 53 | "ethereum-waffle": "3.4.0", 54 | "ethers": "5.4.6", 55 | "hardhat": "2.6.2", 56 | "hardhat-abi-exporter": "2.1.2", 57 | "hardhat-dependency-compiler": "1.1.1", 58 | "hardhat-deploy": "0.7.0-beta.47", 59 | "hardhat-deploy-ethers": "0.3.0-beta.10", 60 | "hardhat-gas-reporter": "1.0.4", 61 | "hardhat-log-remover": "2.0.2", 62 | "mocha": "9.0.3", 63 | "mocha-junit-reporter": "2.0.0", 64 | "prettier": "2.4.1", 65 | "prettier-plugin-solidity": "1.0.0-beta.18", 66 | "solc": "0.8.6", 67 | "solc-0.8": "npm:solc@0.8.6", 68 | "solhint": "3.3.3", 69 | "solidity-coverage": "0.7.16", 70 | "solidity-docgen": "0.5.15", 71 | "ts-generator": "0.1.1", 72 | "ts-node": "9.1.1", 73 | "typescript": "4.2.3" 74 | }, 75 | "files": [ 76 | "LICENSE", 77 | "abis/**", 78 | "artifacts/**", 79 | "contracts/**" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /contracts/test/TwabLibraryExposed.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../libraries/TwabLib.sol"; 6 | import "../libraries/RingBufferLib.sol"; 7 | 8 | /// @title TwabLibExposed contract to test TwabLib library 9 | /// @author PoolTogether Inc. 10 | contract TwabLibExposed { 11 | uint24 public constant MAX_CARDINALITY = 16777215; 12 | 13 | using TwabLib for ObservationLib.Observation[MAX_CARDINALITY]; 14 | 15 | TwabLib.Account account; 16 | 17 | event Updated( 18 | TwabLib.AccountDetails accountDetails, 19 | ObservationLib.Observation twab, 20 | bool isNew 21 | ); 22 | 23 | function details() external view returns (TwabLib.AccountDetails memory) { 24 | return account.details; 25 | } 26 | 27 | function twabs() external view returns (ObservationLib.Observation[] memory) { 28 | ObservationLib.Observation[] memory _twabs = new ObservationLib.Observation[]( 29 | account.details.cardinality 30 | ); 31 | 32 | for (uint256 i = 0; i < _twabs.length; i++) { 33 | _twabs[i] = account.twabs[i]; 34 | } 35 | 36 | return _twabs; 37 | } 38 | 39 | function increaseBalance(uint256 _amount, uint32 _currentTime) 40 | external 41 | returns ( 42 | TwabLib.AccountDetails memory accountDetails, 43 | ObservationLib.Observation memory twab, 44 | bool isNew 45 | ) 46 | { 47 | (accountDetails, twab, isNew) = TwabLib.increaseBalance(account, uint208(_amount), _currentTime); 48 | account.details = accountDetails; 49 | emit Updated(accountDetails, twab, isNew); 50 | } 51 | 52 | function decreaseBalance( 53 | uint256 _amount, 54 | string memory _revertMessage, 55 | uint32 _currentTime 56 | ) 57 | external 58 | returns ( 59 | TwabLib.AccountDetails memory accountDetails, 60 | ObservationLib.Observation memory twab, 61 | bool isNew 62 | ) 63 | { 64 | (accountDetails, twab, isNew) = TwabLib.decreaseBalance( 65 | account, 66 | uint208(_amount), 67 | _revertMessage, 68 | _currentTime 69 | ); 70 | 71 | account.details = accountDetails; 72 | 73 | emit Updated(accountDetails, twab, isNew); 74 | } 75 | 76 | function getAverageBalanceBetween( 77 | uint32 _startTime, 78 | uint32 _endTime, 79 | uint32 _currentTime 80 | ) external view returns (uint256) { 81 | return 82 | TwabLib.getAverageBalanceBetween( 83 | account.twabs, 84 | account.details, 85 | _startTime, 86 | _endTime, 87 | _currentTime 88 | ); 89 | } 90 | 91 | function oldestTwab() 92 | external 93 | view 94 | returns (uint24 index, ObservationLib.Observation memory twab) 95 | { 96 | return TwabLib.oldestTwab(account.twabs, account.details); 97 | } 98 | 99 | function newestTwab() 100 | external 101 | view 102 | returns (uint24 index, ObservationLib.Observation memory twab) 103 | { 104 | return TwabLib.newestTwab(account.twabs, account.details); 105 | } 106 | 107 | function getBalanceAt(uint32 _target, uint32 _currentTime) external view returns (uint256) { 108 | return TwabLib.getBalanceAt(account.twabs, account.details, _target, _currentTime); 109 | } 110 | 111 | function push(TwabLib.AccountDetails memory _accountDetails) external pure returns (TwabLib.AccountDetails memory) { 112 | return TwabLib.push(_accountDetails); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | PoolTogether Brand 5 | 6 |

7 | 8 |
9 | 10 | # PoolTogether V4 Core Smart Contracts 11 | 12 | ![Tests](https://github.com/pooltogether/v4-core/actions/workflows/main.yml/badge.svg) 13 | [![Coverage Status](https://coveralls.io/repos/github/pooltogether/v4-core/badge.svg?branch=master)](https://coveralls.io/github/pooltogether/v4-core?branch=master) 14 | [![built-with openzeppelin](https://img.shields.io/badge/built%20with-OpenZeppelin-3677FF)](https://docs.openzeppelin.com/) 15 | [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://perso.crans.org/besson/LICENSE.html) 16 | 17 | Have questions or want the latest news? 18 |
Join the PoolTogether Discord or follow us on Twitter: 19 | 20 | [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://pooltogether.com/discord) 21 | [![Twitter](https://badgen.net/badge/icon/twitter?icon=twitter&label)](https://twitter.com/PoolTogether_) 22 | 23 | **Documentation**
24 | https://v4.docs.pooltogether.com 25 | 26 | **Deployments**
27 | - [Ethereum](https://v4.docs.pooltogether.com/protocol/deployments/mainnet#mainnet) 28 | - [Polygon](https://v4.docs.pooltogether.com/protocol/deployments/mainnet#polygon) 29 | - [Avalanche](https://v4.docs.pooltogether.com/protocol/deployments/mainnet#avalanche) 30 | - [Optimism](https://v4.docs.pooltogether.com/protocol/deployments/mainnet/#optimism) 31 | 32 | # Overview 33 | - [ControlledToken](/contracts/ControlledToken.sol) 34 | - [DrawBeacon](/contracts/DrawBeacon.sol) 35 | - [DrawBuffer](/contracts/DrawBuffer.sol) 36 | - [DrawCalculator](/contracts/DrawCalculator.sol) 37 | - [EIP2612PermitAndDeposit](/contracts/permit/EIP2612PermitAndDeposit.sol) 38 | - [PrizeDistributionBuffer](/contracts/PrizeDistributionBuffer.sol) 39 | - [PrizeDistributor](/contracts/PrizeDistributor.sol) 40 | - [PrizeSplitStrategy](/contracts/prize-strategy/PrizeSplitStrategy.sol) 41 | - [Reserve](/contracts/Reserve.sol) 42 | - [StakePrizePool](/contracts/prize-pool/StakePrizePool.sol) 43 | - [Ticket](/contracts/Ticket.sol) 44 | - [YieldSourcePrizePool](/contracts/prize-pool/YieldSourcePrizePool.sol) 45 | 46 | Periphery and supporting contracts: 47 | 48 | - https://github.com/pooltogether/v4-periphery 49 | - https://github.com/pooltogether/v4-oracle-timelocks 50 | 51 | 52 | # Getting Started 53 | 54 | The project is made available as a NPM package. 55 | 56 | ```sh 57 | $ yarn add @pooltogether/pooltogether-contracts 58 | ``` 59 | 60 | The repo can be cloned from Github for contributions. 61 | 62 | ```sh 63 | $ git clone https://github.com/pooltogether/v4-core 64 | ``` 65 | 66 | ```sh 67 | $ yarn 68 | ``` 69 | 70 | We use [direnv](https://direnv.net/) to manage environment variables. You'll likely need to install it. 71 | 72 | ```sh 73 | cp .envrc.example .envrc 74 | ``` 75 | 76 | To run fork scripts, deploy or perform any operation with a mainnet/testnet node you will need an Infura API key. 77 | 78 | # Testing 79 | 80 | We use [Hardhat](https://hardhat.dev) and [hardhat-deploy](https://github.com/wighawag/hardhat-deploy) 81 | 82 | To run unit & integration tests: 83 | 84 | ```sh 85 | $ yarn test 86 | ``` 87 | 88 | To run coverage: 89 | 90 | ```sh 91 | $ yarn coverage 92 | ``` 93 | 94 | # Deployment 95 | 96 | ## Testnets 97 | Deployment is maintained in a different [repo](https://github.com/pooltogether/v4-testnet). 98 | 99 | ## Mainnet 100 | Deployment is maintained in a different [repo](https://github.com/pooltogether/v4-mainnet). 101 | -------------------------------------------------------------------------------- /contracts/interfaces/IPrizeDistributionBuffer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./IPrizeDistributionSource.sol"; 6 | 7 | /** @title IPrizeDistributionBuffer 8 | * @author PoolTogether Inc Team 9 | * @notice The PrizeDistributionBuffer interface. 10 | */ 11 | interface IPrizeDistributionBuffer is IPrizeDistributionSource { 12 | /** 13 | * @notice Emit when PrizeDistribution is set. 14 | * @param drawId Draw id 15 | * @param prizeDistribution IPrizeDistributionBuffer.PrizeDistribution 16 | */ 17 | event PrizeDistributionSet( 18 | uint32 indexed drawId, 19 | IPrizeDistributionBuffer.PrizeDistribution prizeDistribution 20 | ); 21 | 22 | /** 23 | * @notice Read a ring buffer cardinality 24 | * @return Ring buffer cardinality 25 | */ 26 | function getBufferCardinality() external view returns (uint32); 27 | 28 | /** 29 | * @notice Read newest PrizeDistribution from prize distributions ring buffer. 30 | * @dev Uses nextDrawIndex to calculate the most recently added PrizeDistribution. 31 | * @return prizeDistribution 32 | * @return drawId 33 | */ 34 | function getNewestPrizeDistribution() 35 | external 36 | view 37 | returns ( 38 | IPrizeDistributionBuffer.PrizeDistribution memory prizeDistribution, 39 | uint32 drawId 40 | ); 41 | 42 | /** 43 | * @notice Read oldest PrizeDistribution from prize distributions ring buffer. 44 | * @dev Finds the oldest Draw by buffer.nextIndex and buffer.lastDrawId 45 | * @return prizeDistribution 46 | * @return drawId 47 | */ 48 | function getOldestPrizeDistribution() 49 | external 50 | view 51 | returns ( 52 | IPrizeDistributionBuffer.PrizeDistribution memory prizeDistribution, 53 | uint32 drawId 54 | ); 55 | 56 | /** 57 | * @notice Gets the PrizeDistributionBuffer for a drawId 58 | * @param drawId drawId 59 | * @return prizeDistribution 60 | */ 61 | function getPrizeDistribution(uint32 drawId) 62 | external 63 | view 64 | returns (IPrizeDistributionBuffer.PrizeDistribution memory); 65 | 66 | /** 67 | * @notice Gets the number of PrizeDistributions stored in the prize distributions ring buffer. 68 | * @dev If no Draws have been pushed, it will return 0. 69 | * @dev If the ring buffer is full, it will return the cardinality. 70 | * @dev Otherwise, it will return the NewestPrizeDistribution index + 1. 71 | * @return Number of PrizeDistributions stored in the prize distributions ring buffer. 72 | */ 73 | function getPrizeDistributionCount() external view returns (uint32); 74 | 75 | /** 76 | * @notice Adds new PrizeDistribution record to ring buffer storage. 77 | * @dev Only callable by the owner or manager 78 | * @param drawId Draw ID linked to PrizeDistribution parameters 79 | * @param prizeDistribution PrizeDistribution parameters struct 80 | */ 81 | function pushPrizeDistribution( 82 | uint32 drawId, 83 | IPrizeDistributionBuffer.PrizeDistribution calldata prizeDistribution 84 | ) external returns (bool); 85 | 86 | /** 87 | * @notice Sets existing PrizeDistribution with new PrizeDistribution parameters in ring buffer storage. 88 | * @dev Retroactively updates an existing PrizeDistribution and should be thought of as a "safety" 89 | fallback. If the manager is setting invalid PrizeDistribution parameters the Owner can update 90 | the invalid parameters with correct parameters. 91 | * @return drawId 92 | */ 93 | function setPrizeDistribution( 94 | uint32 drawId, 95 | IPrizeDistributionBuffer.PrizeDistribution calldata draw 96 | ) external returns (uint32); 97 | } 98 | -------------------------------------------------------------------------------- /contracts/interfaces/IPrizeSplit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "./IControlledToken.sol"; 6 | import "./IPrizePool.sol"; 7 | 8 | /** 9 | * @title Abstract prize split contract for adding unique award distribution to static addresses. 10 | * @author PoolTogether Inc Team 11 | */ 12 | interface IPrizeSplit { 13 | /** 14 | * @notice Emit when an individual prize split is awarded. 15 | * @param user User address being awarded 16 | * @param prizeAwarded Awarded prize amount 17 | * @param token Token address 18 | */ 19 | event PrizeSplitAwarded( 20 | address indexed user, 21 | uint256 prizeAwarded, 22 | IControlledToken indexed token 23 | ); 24 | 25 | /** 26 | * @notice The prize split configuration struct. 27 | * @dev The prize split configuration struct used to award prize splits during distribution. 28 | * @param target Address of recipient receiving the prize split distribution 29 | * @param percentage Percentage of prize split using a 0-1000 range for single decimal precision i.e. 125 = 12.5% 30 | */ 31 | struct PrizeSplitConfig { 32 | address target; 33 | uint16 percentage; 34 | } 35 | 36 | /** 37 | * @notice Emitted when a PrizeSplitConfig config is added or updated. 38 | * @dev Emitted when a PrizeSplitConfig config is added or updated in setPrizeSplits or setPrizeSplit. 39 | * @param target Address of prize split recipient 40 | * @param percentage Percentage of prize split. Must be between 0 and 1000 for single decimal precision 41 | * @param index Index of prize split in the prizeSplts array 42 | */ 43 | event PrizeSplitSet(address indexed target, uint16 percentage, uint256 index); 44 | 45 | /** 46 | * @notice Emitted when a PrizeSplitConfig config is removed. 47 | * @dev Emitted when a PrizeSplitConfig config is removed from the prizeSplits array. 48 | * @param target Index of a previously active prize split config 49 | */ 50 | event PrizeSplitRemoved(uint256 indexed target); 51 | 52 | /** 53 | * @notice Read prize split config from active PrizeSplits. 54 | * @dev Read PrizeSplitConfig struct from prizeSplits array. 55 | * @param prizeSplitIndex Index position of PrizeSplitConfig 56 | * @return PrizeSplitConfig Single prize split config 57 | */ 58 | function getPrizeSplit(uint256 prizeSplitIndex) external view returns (PrizeSplitConfig memory); 59 | 60 | /** 61 | * @notice Read all prize splits configs. 62 | * @dev Read all PrizeSplitConfig structs stored in prizeSplits. 63 | * @return Array of PrizeSplitConfig structs 64 | */ 65 | function getPrizeSplits() external view returns (PrizeSplitConfig[] memory); 66 | 67 | /** 68 | * @notice Get PrizePool address 69 | * @return IPrizePool 70 | */ 71 | function getPrizePool() external view returns (IPrizePool); 72 | 73 | /** 74 | * @notice Set and remove prize split(s) configs. Only callable by owner. 75 | * @dev Set and remove prize split configs by passing a new PrizeSplitConfig structs array. Will remove existing PrizeSplitConfig(s) if passed array length is less than existing prizeSplits length. 76 | * @param newPrizeSplits Array of PrizeSplitConfig structs 77 | */ 78 | function setPrizeSplits(PrizeSplitConfig[] calldata newPrizeSplits) external; 79 | 80 | /** 81 | * @notice Updates a previously set prize split config. 82 | * @dev Updates a prize split config by passing a new PrizeSplitConfig struct and current index position. Limited to contract owner. 83 | * @param prizeStrategySplit PrizeSplitConfig config struct 84 | * @param prizeSplitIndex Index position of PrizeSplitConfig to update 85 | */ 86 | function setPrizeSplit(PrizeSplitConfig memory prizeStrategySplit, uint8 prizeSplitIndex) 87 | external; 88 | } 89 | -------------------------------------------------------------------------------- /contracts/interfaces/IPrizeDistributor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.6; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import "./IDrawBuffer.sol"; 7 | import "./IDrawCalculator.sol"; 8 | 9 | /** @title IPrizeDistributor 10 | * @author PoolTogether Inc Team 11 | * @notice The PrizeDistributor interface. 12 | */ 13 | interface IPrizeDistributor { 14 | 15 | /** 16 | * @notice Emit when user has claimed token from the PrizeDistributor. 17 | * @param user User address receiving draw claim payouts 18 | * @param drawId Draw id that was paid out 19 | * @param payout Payout for draw 20 | */ 21 | event ClaimedDraw(address indexed user, uint32 indexed drawId, uint256 payout); 22 | 23 | /** 24 | * @notice Emit when DrawCalculator is set. 25 | * @param calculator DrawCalculator address 26 | */ 27 | event DrawCalculatorSet(IDrawCalculator indexed calculator); 28 | 29 | /** 30 | * @notice Emit when Token is set. 31 | * @param token Token address 32 | */ 33 | event TokenSet(IERC20 indexed token); 34 | 35 | /** 36 | * @notice Emit when ERC20 tokens are withdrawn. 37 | * @param token ERC20 token transferred. 38 | * @param to Address that received funds. 39 | * @param amount Amount of tokens transferred. 40 | */ 41 | event ERC20Withdrawn(IERC20 indexed token, address indexed to, uint256 amount); 42 | 43 | /** 44 | * @notice Claim prize payout(s) by submitting valid drawId(s) and winning pick indice(s). The user address 45 | is used as the "seed" phrase to generate random numbers. 46 | * @dev The claim function is public and any wallet may execute claim on behalf of another user. 47 | Prizes are always paid out to the designated user account and not the caller (msg.sender). 48 | Claiming prizes is not limited to a single transaction. Reclaiming can be executed 49 | subsequentially if an "optimal" prize was not included in previous claim pick indices. The 50 | payout difference for the new claim is calculated during the award process and transfered to user. 51 | * @param user Address of user to claim awards for. Does NOT need to be msg.sender 52 | * @param drawIds Draw IDs from global DrawBuffer reference 53 | * @param data The data to pass to the draw calculator 54 | * @return Total claim payout. May include calcuations from multiple draws. 55 | */ 56 | function claim( 57 | address user, 58 | uint32[] calldata drawIds, 59 | bytes calldata data 60 | ) external returns (uint256); 61 | 62 | /** 63 | * @notice Read global DrawCalculator address. 64 | * @return IDrawCalculator 65 | */ 66 | function getDrawCalculator() external view returns (IDrawCalculator); 67 | 68 | /** 69 | * @notice Get the amount that a user has already been paid out for a draw 70 | * @param user User address 71 | * @param drawId Draw ID 72 | */ 73 | function getDrawPayoutBalanceOf(address user, uint32 drawId) external view returns (uint256); 74 | 75 | /** 76 | * @notice Read global Ticket address. 77 | * @return IERC20 78 | */ 79 | function getToken() external view returns (IERC20); 80 | 81 | /** 82 | * @notice Sets DrawCalculator reference contract. 83 | * @param newCalculator DrawCalculator address 84 | * @return New DrawCalculator address 85 | */ 86 | function setDrawCalculator(IDrawCalculator newCalculator) external returns (IDrawCalculator); 87 | 88 | /** 89 | * @notice Transfer ERC20 tokens out of contract to recipient address. 90 | * @dev Only callable by contract owner. 91 | * @param token ERC20 token to transfer. 92 | * @param to Recipient of the tokens. 93 | * @param amount Amount of tokens to transfer. 94 | * @return true if operation is successful. 95 | */ 96 | function withdrawERC20( 97 | IERC20 token, 98 | address to, 99 | uint256 amount 100 | ) external returns (bool); 101 | } 102 | -------------------------------------------------------------------------------- /contracts/ControlledToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; 6 | 7 | import "./interfaces/IControlledToken.sol"; 8 | 9 | /** 10 | * @title PoolTogether V4 Controlled ERC20 Token 11 | * @author PoolTogether Inc Team 12 | * @notice ERC20 Tokens with a controller for minting & burning 13 | */ 14 | contract ControlledToken is ERC20Permit, IControlledToken { 15 | /* ============ Global Variables ============ */ 16 | 17 | /// @notice Interface to the contract responsible for controlling mint/burn 18 | address public override immutable controller; 19 | 20 | /// @notice ERC20 controlled token decimals. 21 | uint8 private immutable _decimals; 22 | 23 | /* ============ Events ============ */ 24 | 25 | /// @dev Emitted when contract is deployed 26 | event Deployed(string name, string symbol, uint8 decimals, address indexed controller); 27 | 28 | /* ============ Modifiers ============ */ 29 | 30 | /// @dev Function modifier to ensure that the caller is the controller contract 31 | modifier onlyController() { 32 | require(msg.sender == address(controller), "ControlledToken/only-controller"); 33 | _; 34 | } 35 | 36 | /* ============ Constructor ============ */ 37 | 38 | /// @notice Deploy the Controlled Token with Token Details and the Controller 39 | /// @param _name The name of the Token 40 | /// @param _symbol The symbol for the Token 41 | /// @param decimals_ The number of decimals for the Token 42 | /// @param _controller Address of the Controller contract for minting & burning 43 | constructor( 44 | string memory _name, 45 | string memory _symbol, 46 | uint8 decimals_, 47 | address _controller 48 | ) ERC20Permit("PoolTogether ControlledToken") ERC20(_name, _symbol) { 49 | require(address(_controller) != address(0), "ControlledToken/controller-not-zero-address"); 50 | controller = _controller; 51 | 52 | require(decimals_ > 0, "ControlledToken/decimals-gt-zero"); 53 | _decimals = decimals_; 54 | 55 | emit Deployed(_name, _symbol, decimals_, _controller); 56 | } 57 | 58 | /* ============ External Functions ============ */ 59 | 60 | /// @notice Allows the controller to mint tokens for a user account 61 | /// @dev May be overridden to provide more granular control over minting 62 | /// @param _user Address of the receiver of the minted tokens 63 | /// @param _amount Amount of tokens to mint 64 | function controllerMint(address _user, uint256 _amount) 65 | external 66 | virtual 67 | override 68 | onlyController 69 | { 70 | _mint(_user, _amount); 71 | } 72 | 73 | /// @notice Allows the controller to burn tokens from a user account 74 | /// @dev May be overridden to provide more granular control over burning 75 | /// @param _user Address of the holder account to burn tokens from 76 | /// @param _amount Amount of tokens to burn 77 | function controllerBurn(address _user, uint256 _amount) 78 | external 79 | virtual 80 | override 81 | onlyController 82 | { 83 | _burn(_user, _amount); 84 | } 85 | 86 | /// @notice Allows an operator via the controller to burn tokens on behalf of a user account 87 | /// @dev May be overridden to provide more granular control over operator-burning 88 | /// @param _operator Address of the operator performing the burn action via the controller contract 89 | /// @param _user Address of the holder account to burn tokens from 90 | /// @param _amount Amount of tokens to burn 91 | function controllerBurnFrom( 92 | address _operator, 93 | address _user, 94 | uint256 _amount 95 | ) external virtual override onlyController { 96 | if (_operator != _user) { 97 | _approve(_user, _operator, allowance(_user, _operator) - _amount); 98 | } 99 | 100 | _burn(_user, _amount); 101 | } 102 | 103 | /// @notice Returns the ERC20 controlled token decimals. 104 | /// @dev This value should be equal to the decimals of the token used to deposit into the pool. 105 | /// @return uint8 decimals. 106 | function decimals() public view virtual override returns (uint8) { 107 | return _decimals; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/prize-pool/StakePrizePool.test.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from '@ethersproject/abstract-signer'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 3 | import { expect } from 'chai'; 4 | import { constants, Contract, ContractFactory, utils } from 'ethers'; 5 | import { deployMockContract, MockContract } from 'ethereum-waffle'; 6 | import hardhat from 'hardhat'; 7 | 8 | const { AddressZero } = constants; 9 | const { parseEther: toWei } = utils; 10 | 11 | const debug = require('debug')('ptv3:PrizePool.test'); 12 | 13 | describe('StakePrizePool', function () { 14 | let wallet: SignerWithAddress; 15 | let wallet2: SignerWithAddress; 16 | 17 | let prizePool: Contract; 18 | let erc20token: MockContract; 19 | let erc721token: MockContract; 20 | let stakeToken: Contract; 21 | 22 | let ticket: Contract; 23 | let StakePrizePool: ContractFactory; 24 | 25 | let isConstructorTest = false; 26 | 27 | const deployStakePrizePool = async (stakeTokenAddress: string = stakeToken.address) => { 28 | StakePrizePool = await hardhat.ethers.getContractFactory('StakePrizePool', wallet); 29 | prizePool = await StakePrizePool.deploy(wallet.address, stakeTokenAddress); 30 | 31 | const Ticket = await hardhat.ethers.getContractFactory('Ticket'); 32 | ticket = await Ticket.deploy('name', 'SYMBOL', 18, prizePool.address); 33 | 34 | await prizePool.setTicket(ticket.address); 35 | }; 36 | 37 | beforeEach(async () => { 38 | [wallet, wallet2] = await hardhat.ethers.getSigners(); 39 | debug(`using wallet ${wallet.address}`); 40 | 41 | debug('mocking tokens...'); 42 | const IERC20 = await hardhat.artifacts.readArtifact('IERC20'); 43 | erc20token = await deployMockContract(wallet as Signer, IERC20.abi); 44 | 45 | const IERC721 = await hardhat.artifacts.readArtifact('IERC721'); 46 | erc721token = await deployMockContract(wallet as Signer, IERC721.abi); 47 | 48 | const ERC20Mintable = await hardhat.ethers.getContractFactory('ERC20Mintable'); 49 | stakeToken = await ERC20Mintable.deploy('name', 'SSYMBOL'); 50 | 51 | if (!isConstructorTest) { 52 | await deployStakePrizePool(); 53 | } 54 | }); 55 | 56 | describe('constructor()', () => { 57 | before(() => { 58 | isConstructorTest = true; 59 | }); 60 | 61 | after(() => { 62 | isConstructorTest = false; 63 | }); 64 | 65 | it('should initialize StakePrizePool', async () => { 66 | await deployStakePrizePool(); 67 | 68 | await expect(prizePool.deployTransaction) 69 | .to.emit(prizePool, 'Deployed') 70 | .withArgs(stakeToken.address); 71 | }); 72 | 73 | it('should fail to initialize StakePrizePool if stakeToken is address zero', async () => { 74 | await expect(deployStakePrizePool(AddressZero)).to.be.revertedWith( 75 | 'StakePrizePool/stake-token-not-zero-address', 76 | ); 77 | }); 78 | }); 79 | 80 | describe('_redeem()', () => { 81 | it('should return amount staked', async () => { 82 | const amount = toWei('100'); 83 | 84 | await stakeToken.approve(prizePool.address, amount); 85 | await stakeToken.mint(wallet.address, amount); 86 | 87 | await prizePool.depositTo(wallet.address, amount); 88 | 89 | await expect(prizePool.withdrawFrom(wallet.address, amount)) 90 | .to.emit(prizePool, 'Withdrawal') 91 | .withArgs(wallet.address, wallet.address, ticket.address, amount, amount); 92 | }); 93 | }); 94 | 95 | describe('canAwardExternal()', () => { 96 | it('should not allow the stake award', async () => { 97 | expect(await prizePool.canAwardExternal(stakeToken.address)).to.be.false; 98 | }); 99 | }); 100 | 101 | describe('balance()', () => { 102 | it('should return the staked balance', async () => { 103 | const amount = toWei('100'); 104 | 105 | await stakeToken.approve(prizePool.address, amount); 106 | await stakeToken.mint(wallet.address, amount); 107 | 108 | await prizePool.depositTo(wallet.address, amount); 109 | 110 | expect(await prizePool.callStatic.balance()).to.equal(amount); 111 | }); 112 | }); 113 | 114 | describe('_token()', () => { 115 | it('should return the staked token', async () => { 116 | expect(await prizePool.getToken()).to.equal(stakeToken.address); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /contracts/libraries/ObservationLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 6 | 7 | import "./OverflowSafeComparatorLib.sol"; 8 | import "./RingBufferLib.sol"; 9 | 10 | /** 11 | * @title Observation Library 12 | * @notice This library allows one to store an array of timestamped values and efficiently binary search them. 13 | * @dev Largely pulled from Uniswap V3 Oracle.sol: https://github.com/Uniswap/v3-core/blob/c05a0e2c8c08c460fb4d05cfdda30b3ad8deeaac/contracts/libraries/Oracle.sol 14 | * @author PoolTogether Inc. 15 | */ 16 | library ObservationLib { 17 | using OverflowSafeComparatorLib for uint32; 18 | using SafeCast for uint256; 19 | 20 | /// @notice The maximum number of observations 21 | uint24 public constant MAX_CARDINALITY = 16777215; // 2**24 22 | 23 | /** 24 | * @notice Observation, which includes an amount and timestamp. 25 | * @param amount `amount` at `timestamp`. 26 | * @param timestamp Recorded `timestamp`. 27 | */ 28 | struct Observation { 29 | uint224 amount; 30 | uint32 timestamp; 31 | } 32 | 33 | /** 34 | * @notice Fetches Observations `beforeOrAt` and `atOrAfter` a `_target`, eg: where [`beforeOrAt`, `atOrAfter`] is satisfied. 35 | * The result may be the same Observation, or adjacent Observations. 36 | * @dev The answer must be contained in the array used when the target is located within the stored Observation. 37 | * boundaries: older than the most recent Observation and younger, or the same age as, the oldest Observation. 38 | * @dev If `_newestObservationIndex` is less than `_oldestObservationIndex`, it means that we've wrapped around the circular buffer. 39 | * So the most recent observation will be at `_oldestObservationIndex + _cardinality - 1`, at the beginning of the circular buffer. 40 | * @param _observations List of Observations to search through. 41 | * @param _newestObservationIndex Index of the newest Observation. Right side of the circular buffer. 42 | * @param _oldestObservationIndex Index of the oldest Observation. Left side of the circular buffer. 43 | * @param _target Timestamp at which we are searching the Observation. 44 | * @param _cardinality Cardinality of the circular buffer we are searching through. 45 | * @param _time Timestamp at which we perform the binary search. 46 | * @return beforeOrAt Observation recorded before, or at, the target. 47 | * @return atOrAfter Observation recorded at, or after, the target. 48 | */ 49 | function binarySearch( 50 | Observation[MAX_CARDINALITY] storage _observations, 51 | uint24 _newestObservationIndex, 52 | uint24 _oldestObservationIndex, 53 | uint32 _target, 54 | uint24 _cardinality, 55 | uint32 _time 56 | ) internal view returns (Observation memory beforeOrAt, Observation memory atOrAfter) { 57 | uint256 leftSide = _oldestObservationIndex; 58 | uint256 rightSide = _newestObservationIndex < leftSide 59 | ? leftSide + _cardinality - 1 60 | : _newestObservationIndex; 61 | uint256 currentIndex; 62 | 63 | while (true) { 64 | // We start our search in the middle of the `leftSide` and `rightSide`. 65 | // After each iteration, we narrow down the search to the left or the right side while still starting our search in the middle. 66 | currentIndex = (leftSide + rightSide) / 2; 67 | 68 | beforeOrAt = _observations[uint24(RingBufferLib.wrap(currentIndex, _cardinality))]; 69 | uint32 beforeOrAtTimestamp = beforeOrAt.timestamp; 70 | 71 | // We've landed on an uninitialized timestamp, keep searching higher (more recently). 72 | if (beforeOrAtTimestamp == 0) { 73 | leftSide = currentIndex + 1; 74 | continue; 75 | } 76 | 77 | atOrAfter = _observations[uint24(RingBufferLib.nextIndex(currentIndex, _cardinality))]; 78 | 79 | bool targetAtOrAfter = beforeOrAtTimestamp.lte(_target, _time); 80 | 81 | // Check if we've found the corresponding Observation. 82 | if (targetAtOrAfter && _target.lte(atOrAfter.timestamp, _time)) { 83 | break; 84 | } 85 | 86 | // If `beforeOrAtTimestamp` is greater than `_target`, then we keep searching lower. To the left of the current index. 87 | if (!targetAtOrAfter) { 88 | rightSide = currentIndex - 1; 89 | } else { 90 | // Otherwise, we keep searching higher. To the left of the current index. 91 | leftSide = currentIndex + 1; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/prize-pool/YieldSourcePrizePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/utils/Address.sol"; 8 | import "@pooltogether/yield-source-interface/contracts/IYieldSource.sol"; 9 | 10 | import "./PrizePool.sol"; 11 | 12 | /** 13 | * @title PoolTogether V4 YieldSourcePrizePool 14 | * @author PoolTogether Inc Team 15 | * @notice The Yield Source Prize Pool uses a yield source contract to generate prizes. 16 | * Funds that are deposited into the prize pool are then deposited into a yield source. (i.e. Aave, Compound, etc...) 17 | */ 18 | contract YieldSourcePrizePool is PrizePool { 19 | using SafeERC20 for IERC20; 20 | using Address for address; 21 | 22 | /// @notice Address of the yield source. 23 | IYieldSource public immutable yieldSource; 24 | 25 | /// @dev Emitted when yield source prize pool is deployed. 26 | /// @param yieldSource Address of the yield source. 27 | event Deployed(address indexed yieldSource); 28 | 29 | /// @notice Emitted when stray deposit token balance in this contract is swept 30 | /// @param amount The amount that was swept 31 | event Swept(uint256 amount); 32 | 33 | /// @notice Deploy the Prize Pool and Yield Service with the required contract connections 34 | /// @param _owner Address of the Yield Source Prize Pool owner 35 | /// @param _yieldSource Address of the yield source 36 | constructor(address _owner, IYieldSource _yieldSource) PrizePool(_owner) { 37 | require( 38 | address(_yieldSource) != address(0), 39 | "YieldSourcePrizePool/yield-source-not-zero-address" 40 | ); 41 | 42 | yieldSource = _yieldSource; 43 | 44 | // A hack to determine whether it's an actual yield source 45 | (bool succeeded, bytes memory data) = address(_yieldSource).staticcall( 46 | abi.encodePacked(_yieldSource.depositToken.selector) 47 | ); 48 | address resultingAddress; 49 | if (data.length > 0) { 50 | resultingAddress = abi.decode(data, (address)); 51 | } 52 | require(succeeded && resultingAddress != address(0), "YieldSourcePrizePool/invalid-yield-source"); 53 | 54 | emit Deployed(address(_yieldSource)); 55 | } 56 | 57 | /// @notice Sweeps any stray balance of deposit tokens into the yield source. 58 | /// @dev This becomes prize money 59 | function sweep() external nonReentrant onlyOwner { 60 | uint256 balance = _token().balanceOf(address(this)); 61 | _supply(balance); 62 | 63 | emit Swept(balance); 64 | } 65 | 66 | /// @notice Determines whether the passed token can be transferred out as an external award. 67 | /// @dev Different yield sources will hold the deposits as another kind of token: such a Compound's cToken. The 68 | /// prize strategy should not be allowed to move those tokens. 69 | /// @param _externalToken The address of the token to check 70 | /// @return True if the token may be awarded, false otherwise 71 | function _canAwardExternal(address _externalToken) internal view override returns (bool) { 72 | IYieldSource _yieldSource = yieldSource; 73 | return ( 74 | _externalToken != address(_yieldSource) && 75 | _externalToken != _yieldSource.depositToken() 76 | ); 77 | } 78 | 79 | /// @notice Returns the total balance (in asset tokens). This includes the deposits and interest. 80 | /// @return The underlying balance of asset tokens 81 | function _balance() internal override returns (uint256) { 82 | return yieldSource.balanceOfToken(address(this)); 83 | } 84 | 85 | /// @notice Returns the address of the ERC20 asset token used for deposits. 86 | /// @return Address of the ERC20 asset token. 87 | function _token() internal view override returns (IERC20) { 88 | return IERC20(yieldSource.depositToken()); 89 | } 90 | 91 | /// @notice Supplies asset tokens to the yield source. 92 | /// @param _mintAmount The amount of asset tokens to be supplied 93 | function _supply(uint256 _mintAmount) internal override { 94 | _token().safeIncreaseAllowance(address(yieldSource), _mintAmount); 95 | yieldSource.supplyTokenTo(_mintAmount, address(this)); 96 | } 97 | 98 | /// @notice Redeems asset tokens from the yield source. 99 | /// @param _redeemAmount The amount of yield-bearing tokens to be redeemed 100 | /// @return The actual amount of tokens that were redeemed. 101 | function _redeem(uint256 _redeemAmount) internal override returns (uint256) { 102 | return yieldSource.redeemToken(_redeemAmount); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/libraries/ObservationLib.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Contract, ContractFactory } from 'ethers'; 3 | import { ethers } from 'hardhat'; 4 | 5 | describe('ObservationLib', () => { 6 | let observationLib: Contract; 7 | 8 | beforeEach(async () => { 9 | const observationLibFactory: ContractFactory = await ethers.getContractFactory( 10 | 'ObservationLibHarness', 11 | ); 12 | 13 | observationLib = await observationLibFactory.deploy(); 14 | }); 15 | 16 | describe('binarySearch()', () => { 17 | const currentTime = 1000; // for overflow checks 18 | 19 | context('with two observations', () => { 20 | let newestIndex: number; 21 | let oldestIndex: number; 22 | let cardinality: number; 23 | 24 | beforeEach(async () => { 25 | await observationLib.setObservations([ 26 | { amount: 10, timestamp: 20 }, 27 | { amount: 20, timestamp: 30 }, 28 | ]); 29 | newestIndex = 1; 30 | oldestIndex = 0; 31 | cardinality = 3; // if the buffer hasn't wrapped, its cardinality will be +1 for the # of items 32 | }); 33 | 34 | /* 35 | timestamp must lie within range 36 | 37 | t = target 38 | [ = oldest timestamp 39 | ] = newest timestamp 40 | 41 | t[ ] 42 | [ t ] 43 | [ t] 44 | 45 | */ 46 | 47 | it('should retrieve when timestamp matches first', async () => { 48 | const search = await observationLib.binarySearch(1, 0, 20, 3, currentTime); 49 | 50 | expect(search[0].timestamp).to.equal(20); 51 | expect(search[1].timestamp).to.equal(30); 52 | }); 53 | 54 | it('should retrieve when timestamp is in middle', async () => { 55 | const search = await observationLib.binarySearch(1, 0, 25, 3, currentTime); 56 | 57 | expect(search[0].timestamp).to.equal(20); 58 | expect(search[1].timestamp).to.equal(30); 59 | }); 60 | 61 | it('should retrieve when timestamp matches second', async () => { 62 | const search = await observationLib.binarySearch(1, 0, 30, 3, currentTime); 63 | 64 | expect(search[0].timestamp).to.equal(20); 65 | expect(search[1].timestamp).to.equal(30); 66 | }); 67 | }); 68 | 69 | context('with observations that have wrapped', async () => { 70 | let newestIndex: number; 71 | let oldestIndex: number; 72 | let cardinality: number; 73 | 74 | beforeEach(async () => { 75 | await observationLib.setObservations([ 76 | { amount: 10, timestamp: 60 }, 77 | { amount: 20, timestamp: 30 }, 78 | { amount: 10, timestamp: 40 }, 79 | { amount: 10, timestamp: 50 }, 80 | ]); 81 | 82 | newestIndex = 0; 83 | oldestIndex = 1; 84 | cardinality = 4; // once the buffer wraps it's cardinality will be same as # of elements 85 | }); 86 | 87 | it('should retrieve when timestamp matches first', async () => { 88 | const search = await observationLib.binarySearch( 89 | newestIndex, 90 | oldestIndex, 91 | 30, 92 | cardinality, 93 | currentTime, 94 | ); 95 | 96 | expect(search[0].timestamp).to.equal(30); 97 | expect(search[1].timestamp).to.equal(40); 98 | }); 99 | 100 | it('should retrieve when timestamp is in middle', async () => { 101 | const search = await observationLib.binarySearch( 102 | newestIndex, 103 | oldestIndex, 104 | 45, 105 | cardinality, 106 | currentTime, 107 | ); 108 | 109 | expect(search[0].timestamp).to.equal(40); 110 | expect(search[1].timestamp).to.equal(50); 111 | }); 112 | 113 | it('should retrieve when timestamp matches second', async () => { 114 | const search = await observationLib.binarySearch( 115 | newestIndex, 116 | oldestIndex, 117 | 50, 118 | cardinality, 119 | currentTime, 120 | ); 121 | 122 | expect(search[0].timestamp).to.equal(40); 123 | expect(search[1].timestamp).to.equal(50); 124 | }); 125 | 126 | it('should retrieve when in second half of binary search', async () => { 127 | const search = await observationLib.binarySearch( 128 | newestIndex, 129 | oldestIndex, 130 | 55, 131 | cardinality, 132 | currentTime, 133 | ); 134 | 135 | expect(search[0].timestamp).to.equal(50); 136 | expect(search[1].timestamp).to.equal(60); 137 | }); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/libraries/DrawRingBufferLib.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import { expect } from 'chai'; 3 | import { Contract, ContractFactory } from 'ethers'; 4 | import { ethers } from 'hardhat'; 5 | 6 | const { getSigners } = ethers; 7 | 8 | describe('DrawRingBufferLib', () => { 9 | let DrawRingBufferLib: Contract; 10 | let DrawRingBufferLibFactory: ContractFactory; 11 | 12 | let wallet1: SignerWithAddress; 13 | let wallet2: SignerWithAddress; 14 | 15 | before(async () => { 16 | [wallet1, wallet2] = await getSigners(); 17 | DrawRingBufferLibFactory = await ethers.getContractFactory('DrawRingBufferLibHarness'); 18 | }); 19 | 20 | beforeEach(async () => { 21 | DrawRingBufferLib = await DrawRingBufferLibFactory.deploy('255'); 22 | }); 23 | 24 | describe('isInitialized()', () => { 25 | it('should return TRUE to signal an initalized DrawBuffer', async () => { 26 | expect( 27 | await DrawRingBufferLib._isInitialized({ 28 | lastDrawId: 1, 29 | nextIndex: 1, 30 | cardinality: 256, 31 | }), 32 | ).to.eql(true); 33 | }); 34 | 35 | it('should return FALSE to signal an uninitalized DrawBuffer', async () => { 36 | expect( 37 | await DrawRingBufferLib._isInitialized({ 38 | lastDrawId: 0, 39 | nextIndex: 0, 40 | cardinality: 256, 41 | }), 42 | ).to.eql(false); 43 | }); 44 | }); 45 | 46 | describe('push()', () => { 47 | it('should return the next valid Buffer struct assuming DrawBuffer with 0 draws', async () => { 48 | const nextBuffer = await DrawRingBufferLib._push( 49 | { 50 | lastDrawId: 0, 51 | nextIndex: 0, 52 | cardinality: 256, 53 | }, 54 | 0, 55 | ); 56 | 57 | expect(nextBuffer.lastDrawId).to.eql(0); 58 | expect(nextBuffer.nextIndex).to.eql(1); 59 | expect(nextBuffer.cardinality).to.eql(256); 60 | }); 61 | 62 | it('should return the next valid Buffer struct assuming DrawBuffer with 1 draws', async () => { 63 | const nextBuffer = await DrawRingBufferLib._push( 64 | { 65 | lastDrawId: 0, 66 | nextIndex: 1, 67 | cardinality: 256, 68 | }, 69 | 1, 70 | ); 71 | 72 | expect(nextBuffer.lastDrawId).to.eql(1); 73 | expect(nextBuffer.nextIndex).to.eql(2); 74 | expect(nextBuffer.cardinality).to.eql(256); 75 | }); 76 | 77 | it('should return the next valid Buffer struct assuming DrawBuffer with 255 draws', async () => { 78 | const nextBuffer = await DrawRingBufferLib._push( 79 | { 80 | lastDrawId: 255, 81 | nextIndex: 255, 82 | cardinality: 256, 83 | }, 84 | 256, 85 | ); 86 | 87 | expect(nextBuffer.lastDrawId).to.eql(256); 88 | expect(nextBuffer.nextIndex).to.eql(0); 89 | expect(nextBuffer.cardinality).to.eql(256); 90 | }); 91 | 92 | it('should fail to create new Buffer struct due to not contiguous Draw ID', async () => { 93 | const Buffer = { 94 | lastDrawId: 0, 95 | nextIndex: 1, 96 | cardinality: 256, 97 | }; 98 | 99 | expect(DrawRingBufferLib._push(Buffer, 4)).to.be.revertedWith('DRB/must-be-contig'); 100 | }); 101 | }); 102 | 103 | describe('getIndex()', () => { 104 | it('should return valid draw index assuming DrawBuffer with 1 draw ', async () => { 105 | const Buffer = { 106 | lastDrawId: 0, 107 | nextIndex: 1, 108 | cardinality: 256, 109 | }; 110 | 111 | expect(await DrawRingBufferLib._getIndex(Buffer, 0)).to.eql(0); 112 | }); 113 | 114 | it('should return valid draw index assuming DrawBuffer with 255 draws', async () => { 115 | const Buffer = { 116 | lastDrawId: 255, 117 | nextIndex: 0, 118 | cardinality: 256, 119 | }; 120 | 121 | expect(await DrawRingBufferLib._getIndex(Buffer, 255)).to.eql(255); 122 | }); 123 | 124 | it('should fail to return index since Draw has not been pushed', async () => { 125 | expect( 126 | DrawRingBufferLib._getIndex( 127 | { 128 | lastDrawId: 1, 129 | nextIndex: 2, 130 | cardinality: 256, 131 | }, 132 | 255, 133 | ), 134 | ).to.be.revertedWith('DRB/future-draw'); 135 | }); 136 | 137 | it('should fail to return index since Draw has expired', async () => { 138 | expect( 139 | DrawRingBufferLib._getIndex( 140 | { 141 | lastDrawId: 256, 142 | nextIndex: 1, 143 | cardinality: 256, 144 | }, 145 | 0, 146 | ), 147 | ).to.be.revertedWith('DRB/expired-draw'); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /contracts/permit/EIP2612PermitAndDeposit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | 9 | import "../interfaces/IPrizePool.sol"; 10 | import "../interfaces/ITicket.sol"; 11 | 12 | /** 13 | * @notice Secp256k1 signature values. 14 | * @param deadline Timestamp at which the signature expires 15 | * @param v `v` portion of the signature 16 | * @param r `r` portion of the signature 17 | * @param s `s` portion of the signature 18 | */ 19 | struct Signature { 20 | uint256 deadline; 21 | uint8 v; 22 | bytes32 r; 23 | bytes32 s; 24 | } 25 | 26 | /** 27 | * @notice Delegate signature to allow delegation of tickets to delegate. 28 | * @param delegate Address to delegate the prize pool tickets to 29 | * @param signature Delegate signature 30 | */ 31 | struct DelegateSignature { 32 | address delegate; 33 | Signature signature; 34 | } 35 | 36 | /// @title Allows users to approve and deposit EIP-2612 compatible tokens into a prize pool in a single transaction. 37 | /// @custom:experimental This contract has not been fully audited yet. 38 | contract EIP2612PermitAndDeposit { 39 | using SafeERC20 for IERC20; 40 | 41 | /** 42 | * @notice Permits this contract to spend on a user's behalf and deposits into the prize pool. 43 | * @dev The `spender` address required by the permit function is the address of this contract. 44 | * @param _prizePool Address of the prize pool to deposit into 45 | * @param _amount Amount of tokens to deposit into the prize pool 46 | * @param _to Address that will receive the tickets 47 | * @param _permitSignature Permit signature 48 | * @param _delegateSignature Delegate signature 49 | */ 50 | function permitAndDepositToAndDelegate( 51 | IPrizePool _prizePool, 52 | uint256 _amount, 53 | address _to, 54 | Signature calldata _permitSignature, 55 | DelegateSignature calldata _delegateSignature 56 | ) external { 57 | ITicket _ticket = _prizePool.getTicket(); 58 | address _token = _prizePool.getToken(); 59 | 60 | IERC20Permit(_token).permit( 61 | msg.sender, 62 | address(this), 63 | _amount, 64 | _permitSignature.deadline, 65 | _permitSignature.v, 66 | _permitSignature.r, 67 | _permitSignature.s 68 | ); 69 | 70 | _depositToAndDelegate( 71 | address(_prizePool), 72 | _ticket, 73 | _token, 74 | _amount, 75 | _to, 76 | _delegateSignature 77 | ); 78 | } 79 | 80 | /** 81 | * @notice Deposits user's token into the prize pool and delegate tickets. 82 | * @param _prizePool Address of the prize pool to deposit into 83 | * @param _amount Amount of tokens to deposit into the prize pool 84 | * @param _to Address that will receive the tickets 85 | * @param _delegateSignature Delegate signature 86 | */ 87 | function depositToAndDelegate( 88 | IPrizePool _prizePool, 89 | uint256 _amount, 90 | address _to, 91 | DelegateSignature calldata _delegateSignature 92 | ) external { 93 | ITicket _ticket = _prizePool.getTicket(); 94 | address _token = _prizePool.getToken(); 95 | 96 | _depositToAndDelegate( 97 | address(_prizePool), 98 | _ticket, 99 | _token, 100 | _amount, 101 | _to, 102 | _delegateSignature 103 | ); 104 | } 105 | 106 | /** 107 | * @notice Deposits user's token into the prize pool and delegate tickets. 108 | * @param _prizePool Address of the prize pool to deposit into 109 | * @param _ticket Address of the ticket minted by the prize pool 110 | * @param _token Address of the token used to deposit into the prize pool 111 | * @param _amount Amount of tokens to deposit into the prize pool 112 | * @param _to Address that will receive the tickets 113 | * @param _delegateSignature Delegate signature 114 | */ 115 | function _depositToAndDelegate( 116 | address _prizePool, 117 | ITicket _ticket, 118 | address _token, 119 | uint256 _amount, 120 | address _to, 121 | DelegateSignature calldata _delegateSignature 122 | ) internal { 123 | _depositTo(_token, msg.sender, _amount, _prizePool, _to); 124 | 125 | Signature memory signature = _delegateSignature.signature; 126 | 127 | _ticket.delegateWithSignature( 128 | _to, 129 | _delegateSignature.delegate, 130 | signature.deadline, 131 | signature.v, 132 | signature.r, 133 | signature.s 134 | ); 135 | } 136 | 137 | /** 138 | * @notice Deposits user's token into the prize pool. 139 | * @param _token Address of the EIP-2612 token to approve and deposit 140 | * @param _owner Token owner's address (Authorizer) 141 | * @param _amount Amount of tokens to deposit 142 | * @param _prizePool Address of the prize pool to deposit into 143 | * @param _to Address that will receive the tickets 144 | */ 145 | function _depositTo( 146 | address _token, 147 | address _owner, 148 | uint256 _amount, 149 | address _prizePool, 150 | address _to 151 | ) internal { 152 | IERC20(_token).safeTransferFrom(_owner, address(this), _amount); 153 | IERC20(_token).safeIncreaseAllowance(_prizePool, _amount); 154 | IPrizePool(_prizePool).depositTo(_to, _amount); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/ControlledToken.test.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from '@ethersproject/abstract-signer'; 2 | import { Contract } from '@ethersproject/contracts'; 3 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 4 | import { expect } from 'chai'; 5 | import { deployMockContract, MockContract } from 'ethereum-waffle'; 6 | import { artifacts, ethers } from 'hardhat'; 7 | 8 | const { constants, getContractFactory, getSigners, utils } = ethers; 9 | const { AddressZero } = constants; 10 | const { parseEther: toWei } = utils; 11 | 12 | let isConstructorTest = false; 13 | 14 | describe('ControlledToken', () => { 15 | let wallet: SignerWithAddress; 16 | let wallet2: SignerWithAddress; 17 | 18 | let controller: MockContract; 19 | 20 | // Conflict between types for `call` and `deploy`, so we use `any` 21 | let token: any; 22 | 23 | const deployToken = async (controllerAddres = controller.address, decimals = 18) => { 24 | const ControlledToken = await getContractFactory('ControlledToken', wallet); 25 | token = await ControlledToken.deploy('Name', 'Symbol', decimals, controllerAddres); 26 | }; 27 | 28 | beforeEach(async () => { 29 | [wallet, wallet2] = await getSigners(); 30 | 31 | const PrizePool = await artifacts.readArtifact('PrizePool'); 32 | controller = await deployMockContract(wallet as Signer, PrizePool.abi); 33 | 34 | if (!isConstructorTest) { 35 | await deployToken(); 36 | } 37 | }); 38 | 39 | describe('constructor()', () => { 40 | beforeEach(async () => { 41 | isConstructorTest = true; 42 | await deployToken(); 43 | }); 44 | 45 | after(async () => { 46 | isConstructorTest = false; 47 | }); 48 | 49 | it('should fail to deploy token if controller is address zero', async () => { 50 | await expect(deployToken(AddressZero)).to.be.revertedWith( 51 | 'ControlledToken/controller-not-zero-address', 52 | ); 53 | }); 54 | 55 | it('should fail to deploy token if decimals is zero', async () => { 56 | await expect(deployToken(controller.address, 0)).to.be.revertedWith( 57 | 'ControlledToken/decimals-gt-zero', 58 | ); 59 | }); 60 | }); 61 | 62 | describe('controllerMint()', () => { 63 | it('should allow the controller to mint tokens', async () => { 64 | const amount = toWei('10'); 65 | 66 | await controller.call(token, 'controllerMint', wallet.address, amount); 67 | 68 | expect(await token.balanceOf(wallet.address)).to.equal(amount); 69 | }); 70 | 71 | it('should only be callable by the controller', async () => { 72 | const amount = toWei('10'); 73 | 74 | await expect(token.controllerMint(wallet.address, amount)).to.be.revertedWith( 75 | 'ControlledToken/only-controller', 76 | ); 77 | }); 78 | }); 79 | 80 | describe('controllerBurn()', () => { 81 | it('should allow the controller to burn tokens', async () => { 82 | const amount = toWei('10'); 83 | 84 | await controller.call(token, 'controllerMint', wallet.address, amount); 85 | expect(await token.balanceOf(wallet.address)).to.equal(amount); 86 | 87 | await controller.call(token, 'controllerBurn', wallet.address, amount); 88 | expect(await token.balanceOf(wallet.address)).to.equal('0'); 89 | }); 90 | 91 | it('should only be callable by the controller', async () => { 92 | const amount = toWei('10'); 93 | 94 | await expect(token.controllerBurn(wallet.address, amount)).to.be.revertedWith( 95 | 'ControlledToken/only-controller', 96 | ); 97 | }); 98 | }); 99 | 100 | describe('controllerBurnFrom()', () => { 101 | it('should allow the controller to burn for someone', async () => { 102 | const amount = toWei('10'); 103 | 104 | await controller.call(token, 'controllerMint', wallet.address, amount); 105 | await token.approve(wallet2.address, amount); 106 | 107 | await controller.call( 108 | token, 109 | 'controllerBurnFrom', 110 | wallet2.address, 111 | wallet.address, 112 | amount, 113 | ); 114 | 115 | expect(await token.balanceOf(wallet.address)).to.equal('0'); 116 | expect(await token.allowance(wallet.address, wallet2.address)).to.equal('0'); 117 | }); 118 | 119 | it('should not allow non-approved users to burn', async () => { 120 | const amount = toWei('10'); 121 | 122 | await controller.call(token, 'controllerMint', wallet.address, amount); 123 | 124 | await expect( 125 | controller.call( 126 | token, 127 | 'controllerBurnFrom', 128 | wallet2.address, 129 | wallet.address, 130 | amount, 131 | ), 132 | ).to.be.revertedWith(''); 133 | }); 134 | 135 | it('should allow a user to burn their own', async () => { 136 | const amount = toWei('10'); 137 | 138 | await controller.call(token, 'controllerMint', wallet.address, amount); 139 | 140 | await controller.call( 141 | token, 142 | 'controllerBurnFrom', 143 | wallet.address, 144 | wallet.address, 145 | amount, 146 | ); 147 | 148 | expect(await token.balanceOf(wallet.address)).to.equal('0'); 149 | }); 150 | 151 | it('should only be callable by the controller', async () => { 152 | const amount = toWei('10'); 153 | 154 | await expect( 155 | token.controllerBurnFrom(wallet2.address, wallet.address, amount), 156 | ).to.be.revertedWith('ControlledToken/only-controller'); 157 | }); 158 | }); 159 | 160 | describe('decimals()', () => { 161 | it('should return the number of decimals', async () => { 162 | expect(await token.decimals()).to.equal(18); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /contracts/PrizeDistributor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@pooltogether/owner-manager-contracts/contracts/Ownable.sol"; 8 | 9 | import "./interfaces/IPrizeDistributor.sol"; 10 | import "./interfaces/IDrawCalculator.sol"; 11 | 12 | /** 13 | * @title PoolTogether V4 PrizeDistributor 14 | * @author PoolTogether Inc Team 15 | * @notice The PrizeDistributor contract holds Tickets (captured interest) and distributes tickets to users with winning draw claims. 16 | PrizeDistributor uses an external IDrawCalculator to validate a users draw claim, before awarding payouts. To prevent users 17 | from reclaiming prizes, a payout history for each draw claim is mapped to user accounts. Reclaiming a draw can occur 18 | if an "optimal" prize was not included in previous claim pick indices and the new claims updated payout is greater then 19 | the previous prize distributor claim payout. 20 | */ 21 | contract PrizeDistributor is IPrizeDistributor, Ownable { 22 | using SafeERC20 for IERC20; 23 | 24 | /* ============ Global Variables ============ */ 25 | 26 | /// @notice DrawCalculator address 27 | IDrawCalculator internal drawCalculator; 28 | 29 | /// @notice Token address 30 | IERC20 internal immutable token; 31 | 32 | /// @notice Maps users => drawId => paid out balance 33 | mapping(address => mapping(uint256 => uint256)) internal userDrawPayouts; 34 | 35 | /* ============ Initialize ============ */ 36 | 37 | /** 38 | * @notice Initialize PrizeDistributor smart contract. 39 | * @param _owner Owner address 40 | * @param _token Token address 41 | * @param _drawCalculator DrawCalculator address 42 | */ 43 | constructor( 44 | address _owner, 45 | IERC20 _token, 46 | IDrawCalculator _drawCalculator 47 | ) Ownable(_owner) { 48 | _setDrawCalculator(_drawCalculator); 49 | require(address(_token) != address(0), "PrizeDistributor/token-not-zero-address"); 50 | token = _token; 51 | emit TokenSet(_token); 52 | } 53 | 54 | /* ============ External Functions ============ */ 55 | 56 | /// @inheritdoc IPrizeDistributor 57 | function claim( 58 | address _user, 59 | uint32[] calldata _drawIds, 60 | bytes calldata _data 61 | ) external override returns (uint256) { 62 | 63 | uint256 totalPayout; 64 | 65 | (uint256[] memory drawPayouts, ) = drawCalculator.calculate(_user, _drawIds, _data); // neglect the prizeCounts since we are not interested in them here 66 | 67 | uint256 drawPayoutsLength = drawPayouts.length; 68 | for (uint256 payoutIndex = 0; payoutIndex < drawPayoutsLength; payoutIndex++) { 69 | uint32 drawId = _drawIds[payoutIndex]; 70 | uint256 payout = drawPayouts[payoutIndex]; 71 | uint256 oldPayout = _getDrawPayoutBalanceOf(_user, drawId); 72 | uint256 payoutDiff = 0; 73 | 74 | // helpfully short-circuit, in case the user screwed something up. 75 | require(payout > oldPayout, "PrizeDistributor/zero-payout"); 76 | 77 | unchecked { 78 | payoutDiff = payout - oldPayout; 79 | } 80 | 81 | _setDrawPayoutBalanceOf(_user, drawId, payout); 82 | 83 | totalPayout += payoutDiff; 84 | 85 | emit ClaimedDraw(_user, drawId, payoutDiff); 86 | } 87 | 88 | _awardPayout(_user, totalPayout); 89 | 90 | return totalPayout; 91 | } 92 | 93 | /// @inheritdoc IPrizeDistributor 94 | function withdrawERC20( 95 | IERC20 _erc20Token, 96 | address _to, 97 | uint256 _amount 98 | ) external override onlyOwner returns (bool) { 99 | require(_to != address(0), "PrizeDistributor/recipient-not-zero-address"); 100 | require(address(_erc20Token) != address(0), "PrizeDistributor/ERC20-not-zero-address"); 101 | 102 | _erc20Token.safeTransfer(_to, _amount); 103 | 104 | emit ERC20Withdrawn(_erc20Token, _to, _amount); 105 | 106 | return true; 107 | } 108 | 109 | /// @inheritdoc IPrizeDistributor 110 | function getDrawCalculator() external view override returns (IDrawCalculator) { 111 | return drawCalculator; 112 | } 113 | 114 | /// @inheritdoc IPrizeDistributor 115 | function getDrawPayoutBalanceOf(address _user, uint32 _drawId) 116 | external 117 | view 118 | override 119 | returns (uint256) 120 | { 121 | return _getDrawPayoutBalanceOf(_user, _drawId); 122 | } 123 | 124 | /// @inheritdoc IPrizeDistributor 125 | function getToken() external view override returns (IERC20) { 126 | return token; 127 | } 128 | 129 | /// @inheritdoc IPrizeDistributor 130 | function setDrawCalculator(IDrawCalculator _newCalculator) 131 | external 132 | override 133 | onlyOwner 134 | returns (IDrawCalculator) 135 | { 136 | _setDrawCalculator(_newCalculator); 137 | return _newCalculator; 138 | } 139 | 140 | /* ============ Internal Functions ============ */ 141 | 142 | function _getDrawPayoutBalanceOf(address _user, uint32 _drawId) 143 | internal 144 | view 145 | returns (uint256) 146 | { 147 | return userDrawPayouts[_user][_drawId]; 148 | } 149 | 150 | function _setDrawPayoutBalanceOf( 151 | address _user, 152 | uint32 _drawId, 153 | uint256 _payout 154 | ) internal { 155 | userDrawPayouts[_user][_drawId] = _payout; 156 | } 157 | 158 | /** 159 | * @notice Sets DrawCalculator reference for individual draw id. 160 | * @param _newCalculator DrawCalculator address 161 | */ 162 | function _setDrawCalculator(IDrawCalculator _newCalculator) internal { 163 | require(address(_newCalculator) != address(0), "PrizeDistributor/calc-not-zero"); 164 | drawCalculator = _newCalculator; 165 | 166 | emit DrawCalculatorSet(_newCalculator); 167 | } 168 | 169 | /** 170 | * @notice Transfer claimed draw(s) total payout to user. 171 | * @param _to User address 172 | * @param _amount Transfer amount 173 | */ 174 | function _awardPayout(address _to, uint256 _amount) internal { 175 | token.safeTransfer(_to, _amount); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /test/libraries/OverflowSafeComparatorLib.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Contract, ContractFactory } from 'ethers'; 3 | import { ethers } from 'hardhat'; 4 | 5 | const { provider } = ethers; 6 | 7 | describe('overflowSafeComparatorLib', () => { 8 | let overflowSafeComparatorLib: Contract; 9 | let currentTimestamp: number; 10 | 11 | beforeEach(async () => { 12 | currentTimestamp = (await provider.getBlock('latest')).timestamp; 13 | 14 | const overflowSafeComparatorLibFactory: ContractFactory = await ethers.getContractFactory( 15 | 'OverflowSafeComparatorLibHarness', 16 | ); 17 | overflowSafeComparatorLib = await overflowSafeComparatorLibFactory.deploy(); 18 | }); 19 | 20 | describe('lt()', () => { 21 | it('should compare timestamp a to timestamp b if no overflow', async () => { 22 | const timestampA = currentTimestamp - 1000; 23 | const timestampB = currentTimestamp - 100; 24 | 25 | expect( 26 | await overflowSafeComparatorLib.ltHarness(timestampA, timestampB, currentTimestamp), 27 | ).to.equal(true); 28 | }); 29 | 30 | it('should return false if timestamp a is equal to timestamp b', async () => { 31 | const timestampA = currentTimestamp - 1000; 32 | const timestampB = timestampA; 33 | 34 | expect( 35 | await overflowSafeComparatorLib.ltHarness(timestampA, timestampB, currentTimestamp), 36 | ).to.equal(false); 37 | }); 38 | 39 | it('should compare timestamp a to timestamp b if b has overflowed', async () => { 40 | const timestampA = currentTimestamp - 1000; 41 | const timestampB = currentTimestamp + 1000; 42 | 43 | expect( 44 | await overflowSafeComparatorLib.ltHarness(timestampA, timestampB, currentTimestamp), 45 | ).to.equal(false); 46 | }); 47 | 48 | it('should compare timestamp a to timestamp b if a has overflowed', async () => { 49 | const timestampA = currentTimestamp + 1000; 50 | const timestampB = currentTimestamp - 1000; 51 | 52 | expect( 53 | await overflowSafeComparatorLib.ltHarness(timestampA, timestampB, currentTimestamp), 54 | ).to.equal(true); 55 | }); 56 | 57 | it('should return false if timestamps have overflowed and timestamp a is equal to timestamp b', async () => { 58 | const timestampA = currentTimestamp + 1000; 59 | const timestampB = timestampA; 60 | 61 | expect( 62 | await overflowSafeComparatorLib.ltHarness(timestampA, timestampB, currentTimestamp), 63 | ).to.equal(false); 64 | }); 65 | }); 66 | 67 | describe('lte()', () => { 68 | it('should compare timestamp a to timestamp b if no overflow', async () => { 69 | const timestampA = currentTimestamp - 1000; 70 | const timestampB = currentTimestamp - 100; 71 | 72 | expect( 73 | await overflowSafeComparatorLib.lteHarness( 74 | timestampA, 75 | timestampB, 76 | currentTimestamp, 77 | ), 78 | ).to.equal(true); 79 | }); 80 | 81 | it('should return true if timestamp a is equal to timestamp b', async () => { 82 | const timestampA = currentTimestamp - 1000; 83 | const timestampB = timestampA; 84 | 85 | expect( 86 | await overflowSafeComparatorLib.lteHarness( 87 | timestampA, 88 | timestampB, 89 | currentTimestamp, 90 | ), 91 | ).to.equal(true); 92 | }); 93 | 94 | it('should compare timestamp a to timestamp b if b has overflowed', async () => { 95 | const timestampA = currentTimestamp - 1000; 96 | const timestampB = currentTimestamp + 1000; 97 | 98 | expect( 99 | await overflowSafeComparatorLib.lteHarness( 100 | timestampA, 101 | timestampB, 102 | currentTimestamp, 103 | ), 104 | ).to.equal(false); 105 | }); 106 | 107 | it('should compare timestamp a to timestamp b if a has overflowed', async () => { 108 | const timestampA = currentTimestamp + 1000; 109 | const timestampB = currentTimestamp - 1000; 110 | 111 | expect( 112 | await overflowSafeComparatorLib.lteHarness( 113 | timestampA, 114 | timestampB, 115 | currentTimestamp, 116 | ), 117 | ).to.equal(true); 118 | }); 119 | 120 | it('should return true if timestamps have overflowed and timestamp a is equal to timestamp b', async () => { 121 | const timestampA = currentTimestamp + 1000; 122 | const timestampB = timestampA; 123 | 124 | expect( 125 | await overflowSafeComparatorLib.lteHarness( 126 | timestampA, 127 | timestampB, 128 | currentTimestamp, 129 | ), 130 | ).to.equal(true); 131 | }); 132 | }); 133 | 134 | describe('checkedSub()', () => { 135 | it('should calculate normally', async () => { 136 | expect(await overflowSafeComparatorLib.checkedSub(10, 4, 10)).to.equal(6); 137 | }); 138 | 139 | it('should handle overflow of a', async () => { 140 | // put in actual times. 141 | const secondTimestamp = 2 ** 8; 142 | const firstTimestamp = 2 ** 32 + (secondTimestamp - 1); // just before the answer overflows 143 | 144 | expect( 145 | await overflowSafeComparatorLib.checkedSub( 146 | firstTimestamp, 147 | secondTimestamp, 148 | firstTimestamp, 149 | ), 150 | ).to.equal(firstTimestamp - secondTimestamp); 151 | }); 152 | 153 | it('should handle overflow of both', async () => { 154 | // put in actual times. 155 | const secondTimestamp = 2 ** 32 + 100; 156 | const firstTimestamp = 2 ** 32 + 200; 157 | 158 | expect( 159 | await overflowSafeComparatorLib.checkedSub( 160 | firstTimestamp, 161 | secondTimestamp, 162 | firstTimestamp, 163 | ), 164 | ).to.equal(firstTimestamp - secondTimestamp); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /contracts/prize-strategy/PrizeSplit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/owner-manager-contracts/contracts/Ownable.sol"; 6 | 7 | import "../interfaces/IPrizeSplit.sol"; 8 | 9 | /** 10 | * @title PrizeSplit Interface 11 | * @author PoolTogether Inc Team 12 | */ 13 | abstract contract PrizeSplit is IPrizeSplit, Ownable { 14 | /* ============ Global Variables ============ */ 15 | PrizeSplitConfig[] internal _prizeSplits; 16 | 17 | uint16 public constant ONE_AS_FIXED_POINT_3 = 1000; 18 | 19 | /* ============ External Functions ============ */ 20 | 21 | /// @inheritdoc IPrizeSplit 22 | function getPrizeSplit(uint256 _prizeSplitIndex) 23 | external 24 | view 25 | override 26 | returns (PrizeSplitConfig memory) 27 | { 28 | return _prizeSplits[_prizeSplitIndex]; 29 | } 30 | 31 | /// @inheritdoc IPrizeSplit 32 | function getPrizeSplits() external view override returns (PrizeSplitConfig[] memory) { 33 | return _prizeSplits; 34 | } 35 | 36 | /// @inheritdoc IPrizeSplit 37 | function setPrizeSplits(PrizeSplitConfig[] calldata _newPrizeSplits) 38 | external 39 | override 40 | onlyOwner 41 | { 42 | uint256 newPrizeSplitsLength = _newPrizeSplits.length; 43 | require(newPrizeSplitsLength <= type(uint8).max, "PrizeSplit/invalid-prizesplits-length"); 44 | 45 | // Add and/or update prize split configs using _newPrizeSplits PrizeSplitConfig structs array. 46 | for (uint256 index = 0; index < newPrizeSplitsLength; index++) { 47 | PrizeSplitConfig memory split = _newPrizeSplits[index]; 48 | 49 | // REVERT when setting the canonical burn address. 50 | require(split.target != address(0), "PrizeSplit/invalid-prizesplit-target"); 51 | 52 | // IF the CURRENT prizeSplits length is below the NEW prizeSplits 53 | // PUSH the PrizeSplit struct to end of the list. 54 | if (_prizeSplits.length <= index) { 55 | _prizeSplits.push(split); 56 | } else { 57 | // ELSE update an existing PrizeSplit struct with new parameters 58 | PrizeSplitConfig memory currentSplit = _prizeSplits[index]; 59 | 60 | // IF new PrizeSplit DOES NOT match the current PrizeSplit 61 | // WRITE to STORAGE with the new PrizeSplit 62 | if ( 63 | split.target != currentSplit.target || 64 | split.percentage != currentSplit.percentage 65 | ) { 66 | _prizeSplits[index] = split; 67 | } else { 68 | continue; 69 | } 70 | } 71 | 72 | // Emit the added/updated prize split config. 73 | emit PrizeSplitSet(split.target, split.percentage, index); 74 | } 75 | 76 | // Remove old prize splits configs. Match storage _prizesSplits.length with the passed newPrizeSplits.length 77 | while (_prizeSplits.length > newPrizeSplitsLength) { 78 | uint256 _index; 79 | unchecked { 80 | _index = _prizeSplits.length - 1; 81 | } 82 | _prizeSplits.pop(); 83 | emit PrizeSplitRemoved(_index); 84 | } 85 | 86 | // Total prize split do not exceed 100% 87 | uint256 totalPercentage = _totalPrizeSplitPercentageAmount(); 88 | require(totalPercentage <= ONE_AS_FIXED_POINT_3, "PrizeSplit/invalid-prizesplit-percentage-total"); 89 | } 90 | 91 | /// @inheritdoc IPrizeSplit 92 | function setPrizeSplit(PrizeSplitConfig memory _prizeSplit, uint8 _prizeSplitIndex) 93 | external 94 | override 95 | onlyOwner 96 | { 97 | require(_prizeSplitIndex < _prizeSplits.length, "PrizeSplit/nonexistent-prizesplit"); 98 | require(_prizeSplit.target != address(0), "PrizeSplit/invalid-prizesplit-target"); 99 | 100 | // Update the prize split config 101 | _prizeSplits[_prizeSplitIndex] = _prizeSplit; 102 | 103 | // Total prize split do not exceed 100% 104 | uint256 totalPercentage = _totalPrizeSplitPercentageAmount(); 105 | require(totalPercentage <= ONE_AS_FIXED_POINT_3, "PrizeSplit/invalid-prizesplit-percentage-total"); 106 | 107 | // Emit updated prize split config 108 | emit PrizeSplitSet( 109 | _prizeSplit.target, 110 | _prizeSplit.percentage, 111 | _prizeSplitIndex 112 | ); 113 | } 114 | 115 | /* ============ Internal Functions ============ */ 116 | 117 | /** 118 | * @notice Calculates total prize split percentage amount. 119 | * @dev Calculates total PrizeSplitConfig percentage(s) amount. Used to check the total does not exceed 100% of award distribution. 120 | * @return Total prize split(s) percentage amount 121 | */ 122 | function _totalPrizeSplitPercentageAmount() internal view returns (uint256) { 123 | uint256 _tempTotalPercentage; 124 | uint256 prizeSplitsLength = _prizeSplits.length; 125 | 126 | for (uint256 index = 0; index < prizeSplitsLength; index++) { 127 | _tempTotalPercentage += _prizeSplits[index].percentage; 128 | } 129 | 130 | return _tempTotalPercentage; 131 | } 132 | 133 | /** 134 | * @notice Distributes prize split(s). 135 | * @dev Distributes prize split(s) by awarding ticket or sponsorship tokens. 136 | * @param _prize Starting prize award amount 137 | * @return The remainder after splits are taken 138 | */ 139 | function _distributePrizeSplits(uint256 _prize) internal returns (uint256) { 140 | uint256 _prizeTemp = _prize; 141 | uint256 prizeSplitsLength = _prizeSplits.length; 142 | 143 | for (uint256 index = 0; index < prizeSplitsLength; index++) { 144 | PrizeSplitConfig memory split = _prizeSplits[index]; 145 | uint256 _splitAmount = (_prize * split.percentage) / 1000; 146 | 147 | // Award the prize split distribution amount. 148 | _awardPrizeSplitAmount(split.target, _splitAmount); 149 | 150 | // Update the remaining prize amount after distributing the prize split percentage. 151 | _prizeTemp -= _splitAmount; 152 | } 153 | 154 | return _prizeTemp; 155 | } 156 | 157 | /** 158 | * @notice Mints ticket or sponsorship tokens to prize split recipient. 159 | * @dev Mints ticket or sponsorship tokens to prize split recipient via the linked PrizePool contract. 160 | * @param _target Recipient of minted tokens 161 | * @param _amount Amount of minted tokens 162 | */ 163 | function _awardPrizeSplitAmount(address _target, uint256 _amount) internal virtual; 164 | } 165 | -------------------------------------------------------------------------------- /contracts/DrawBuffer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/owner-manager-contracts/contracts/Manageable.sol"; 6 | 7 | import "./interfaces/IDrawBuffer.sol"; 8 | import "./interfaces/IDrawBeacon.sol"; 9 | import "./libraries/DrawRingBufferLib.sol"; 10 | 11 | /** 12 | * @title PoolTogether V4 DrawBuffer 13 | * @author PoolTogether Inc Team 14 | * @notice The DrawBuffer provides historical lookups of Draws via a circular ring buffer. 15 | Historical Draws can be accessed on-chain using a drawId to calculate ring buffer storage slot. 16 | The Draw settings can be created by manager/owner and existing Draws can only be updated the owner. 17 | Once a starting Draw has been added to the ring buffer, all following draws must have a sequential Draw ID. 18 | @dev A DrawBuffer store a limited number of Draws before beginning to overwrite (managed via the cardinality) previous Draws. 19 | @dev All mainnet DrawBuffer(s) are updated directly from a DrawBeacon, but non-mainnet DrawBuffer(s) (Matic, Optimism, Arbitrum, etc...) 20 | will receive a cross-chain message, duplicating the mainnet Draw configuration - enabling a prize savings liquidity network. 21 | */ 22 | contract DrawBuffer is IDrawBuffer, Manageable { 23 | using DrawRingBufferLib for DrawRingBufferLib.Buffer; 24 | 25 | /// @notice Draws ring buffer max length. 26 | uint16 public constant MAX_CARDINALITY = 256; 27 | 28 | /// @notice Draws ring buffer array. 29 | IDrawBeacon.Draw[MAX_CARDINALITY] private drawRingBuffer; 30 | 31 | /// @notice Holds ring buffer information 32 | DrawRingBufferLib.Buffer internal bufferMetadata; 33 | 34 | /* ============ Deploy ============ */ 35 | 36 | /** 37 | * @notice Deploy DrawBuffer smart contract. 38 | * @param _owner Address of the owner of the DrawBuffer. 39 | * @param _cardinality Draw ring buffer cardinality. 40 | */ 41 | constructor(address _owner, uint8 _cardinality) Ownable(_owner) { 42 | bufferMetadata.cardinality = _cardinality; 43 | } 44 | 45 | /* ============ External Functions ============ */ 46 | 47 | /// @inheritdoc IDrawBuffer 48 | function getBufferCardinality() external view override returns (uint32) { 49 | return bufferMetadata.cardinality; 50 | } 51 | 52 | /// @inheritdoc IDrawBuffer 53 | function getDraw(uint32 drawId) external view override returns (IDrawBeacon.Draw memory) { 54 | return drawRingBuffer[_drawIdToDrawIndex(bufferMetadata, drawId)]; 55 | } 56 | 57 | /// @inheritdoc IDrawBuffer 58 | function getDraws(uint32[] calldata _drawIds) 59 | external 60 | view 61 | override 62 | returns (IDrawBeacon.Draw[] memory) 63 | { 64 | IDrawBeacon.Draw[] memory draws = new IDrawBeacon.Draw[](_drawIds.length); 65 | DrawRingBufferLib.Buffer memory buffer = bufferMetadata; 66 | 67 | for (uint256 index = 0; index < _drawIds.length; index++) { 68 | draws[index] = drawRingBuffer[_drawIdToDrawIndex(buffer, _drawIds[index])]; 69 | } 70 | 71 | return draws; 72 | } 73 | 74 | /// @inheritdoc IDrawBuffer 75 | function getDrawCount() external view override returns (uint32) { 76 | DrawRingBufferLib.Buffer memory buffer = bufferMetadata; 77 | 78 | if (buffer.lastDrawId == 0) { 79 | return 0; 80 | } 81 | 82 | uint32 bufferNextIndex = buffer.nextIndex; 83 | 84 | if (drawRingBuffer[bufferNextIndex].timestamp != 0) { 85 | return buffer.cardinality; 86 | } else { 87 | return bufferNextIndex; 88 | } 89 | } 90 | 91 | /// @inheritdoc IDrawBuffer 92 | function getNewestDraw() external view override returns (IDrawBeacon.Draw memory) { 93 | return _getNewestDraw(bufferMetadata); 94 | } 95 | 96 | /// @inheritdoc IDrawBuffer 97 | function getOldestDraw() external view override returns (IDrawBeacon.Draw memory) { 98 | // oldest draw should be next available index, otherwise it's at 0 99 | DrawRingBufferLib.Buffer memory buffer = bufferMetadata; 100 | IDrawBeacon.Draw memory draw = drawRingBuffer[buffer.nextIndex]; 101 | 102 | if (draw.timestamp == 0) { 103 | // if draw is not init, then use draw at 0 104 | draw = drawRingBuffer[0]; 105 | } 106 | 107 | return draw; 108 | } 109 | 110 | /// @inheritdoc IDrawBuffer 111 | function pushDraw(IDrawBeacon.Draw memory _draw) 112 | external 113 | override 114 | onlyManagerOrOwner 115 | returns (uint32) 116 | { 117 | return _pushDraw(_draw); 118 | } 119 | 120 | /// @inheritdoc IDrawBuffer 121 | function setDraw(IDrawBeacon.Draw memory _newDraw) external override onlyOwner returns (uint32) { 122 | DrawRingBufferLib.Buffer memory buffer = bufferMetadata; 123 | uint32 index = buffer.getIndex(_newDraw.drawId); 124 | drawRingBuffer[index] = _newDraw; 125 | emit DrawSet(_newDraw.drawId, _newDraw); 126 | return _newDraw.drawId; 127 | } 128 | 129 | /* ============ Internal Functions ============ */ 130 | 131 | /** 132 | * @notice Convert a Draw.drawId to a Draws ring buffer index pointer. 133 | * @dev The getNewestDraw.drawId() is used to calculate a Draws ID delta position. 134 | * @param _drawId Draw.drawId 135 | * @return Draws ring buffer index pointer 136 | */ 137 | function _drawIdToDrawIndex(DrawRingBufferLib.Buffer memory _buffer, uint32 _drawId) 138 | internal 139 | pure 140 | returns (uint32) 141 | { 142 | return _buffer.getIndex(_drawId); 143 | } 144 | 145 | /** 146 | * @notice Read newest Draw from the draws ring buffer. 147 | * @dev Uses the lastDrawId to calculate the most recently added Draw. 148 | * @param _buffer Draw ring buffer 149 | * @return IDrawBeacon.Draw 150 | */ 151 | function _getNewestDraw(DrawRingBufferLib.Buffer memory _buffer) 152 | internal 153 | view 154 | returns (IDrawBeacon.Draw memory) 155 | { 156 | return drawRingBuffer[_buffer.getIndex(_buffer.lastDrawId)]; 157 | } 158 | 159 | /** 160 | * @notice Push Draw onto draws ring buffer history. 161 | * @dev Push new draw onto draws list via authorized manager or owner. 162 | * @param _newDraw IDrawBeacon.Draw 163 | * @return Draw.drawId 164 | */ 165 | function _pushDraw(IDrawBeacon.Draw memory _newDraw) internal returns (uint32) { 166 | DrawRingBufferLib.Buffer memory _buffer = bufferMetadata; 167 | drawRingBuffer[_buffer.nextIndex] = _newDraw; 168 | bufferMetadata = _buffer.push(_newDraw.drawId); 169 | 170 | emit DrawSet(_newDraw.drawId, _newDraw); 171 | 172 | return _newDraw.drawId; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /test/prize-pool/YieldSourcePrizePool.test.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from '@ethersproject/abstract-signer'; 2 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 3 | import { expect } from 'chai'; 4 | import { BigNumber, constants, Contract, ContractFactory, utils } from 'ethers'; 5 | import { deployMockContract, MockContract } from 'ethereum-waffle'; 6 | import { artifacts, ethers } from 'hardhat'; 7 | 8 | const { AddressZero } = constants; 9 | const { getContractFactory, getSigners } = ethers; 10 | const { parseEther: toWei } = utils; 11 | 12 | const debug = require('debug')('ptv3:YieldSourcePrizePool.test'); 13 | 14 | describe('YieldSourcePrizePool', function () { 15 | let wallet: SignerWithAddress; 16 | let wallet2: SignerWithAddress; 17 | 18 | let prizePool: Contract; 19 | let depositToken: Contract; 20 | let yieldSource: MockContract; 21 | let ticket: Contract; 22 | let YieldSourcePrizePool: ContractFactory; 23 | 24 | let isConstructorTest = false; 25 | 26 | const deployYieldSourcePrizePool = async (yieldSourceAddress: string = yieldSource.address) => { 27 | YieldSourcePrizePool = await getContractFactory('YieldSourcePrizePool', wallet); 28 | prizePool = await YieldSourcePrizePool.deploy(wallet.address, yieldSourceAddress); 29 | 30 | const Ticket = await getContractFactory('Ticket'); 31 | ticket = await Ticket.deploy('name', 'SYMBOL', 18, prizePool.address); 32 | 33 | await prizePool.setPrizeStrategy(wallet2.address); 34 | await prizePool.setTicket(ticket.address); 35 | }; 36 | 37 | const depositTo = async (amount: BigNumber) => { 38 | await yieldSource.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); 39 | 40 | await depositToken.approve(prizePool.address, amount); 41 | await depositToken.mint(wallet.address, amount); 42 | await prizePool.depositTo(wallet.address, amount); 43 | }; 44 | 45 | beforeEach(async () => { 46 | [wallet, wallet2] = await getSigners(); 47 | debug(`using wallet ${wallet.address}`); 48 | 49 | debug('creating token...'); 50 | const ERC20MintableContract = await getContractFactory('ERC20Mintable', wallet); 51 | depositToken = await ERC20MintableContract.deploy('Token', 'TOKE'); 52 | 53 | debug('creating yield source mock...'); 54 | const IYieldSource = await artifacts.readArtifact('IYieldSource'); 55 | yieldSource = await deployMockContract(wallet as Signer, IYieldSource.abi); 56 | await yieldSource.mock.depositToken.returns(depositToken.address); 57 | 58 | await deployYieldSourcePrizePool(); 59 | }); 60 | 61 | describe('constructor()', () => { 62 | it('should deploy correctly', async () => { 63 | await expect(prizePool.deployTransaction) 64 | .to.emit(prizePool, 'Deployed') 65 | .withArgs(yieldSource.address); 66 | 67 | expect(await prizePool.yieldSource()).to.equal(yieldSource.address); 68 | }); 69 | 70 | it('should require the yield source', async () => { 71 | await expect( 72 | YieldSourcePrizePool.deploy(wallet.address, AddressZero), 73 | ).to.be.revertedWith('YieldSourcePrizePool/yield-source-not-zero-address'); 74 | }); 75 | 76 | it('should require a valid yield source', async () => { 77 | await expect( 78 | YieldSourcePrizePool.deploy(wallet.address, prizePool.address), 79 | ).to.be.revertedWith('YieldSourcePrizePool/invalid-yield-source'); 80 | }); 81 | 82 | it('should require a valid yield source', async () => { 83 | await yieldSource.mock.depositToken.returns(AddressZero); 84 | await expect( 85 | YieldSourcePrizePool.deploy(wallet.address, yieldSource.address), 86 | ).to.be.revertedWith('YieldSourcePrizePool/invalid-yield-source'); 87 | }); 88 | }); 89 | 90 | describe('supply()', async () => { 91 | it('should supply assets to the yield source', async () => { 92 | const amount = toWei('10'); 93 | 94 | await depositTo(amount); 95 | 96 | expect(await ticket.balanceOf(wallet.address)).to.equal(amount); 97 | }); 98 | }); 99 | 100 | describe('balance()', async () => { 101 | it('should return the total underlying balance of asset tokens', async () => { 102 | const amount = toWei('10'); 103 | 104 | await depositTo(amount); 105 | 106 | await yieldSource.mock.balanceOfToken.withArgs(prizePool.address).returns(amount); 107 | 108 | expect(await prizePool.callStatic.balance()).to.equal(amount); 109 | }); 110 | }); 111 | 112 | describe('redeem()', async () => { 113 | it('should redeem assets from the yield source', async () => { 114 | const amount = toWei('99'); 115 | 116 | await depositToken.approve(prizePool.address, amount); 117 | await depositToken.mint(wallet.address, amount); 118 | await yieldSource.mock.supplyTokenTo.withArgs(amount, prizePool.address).returns(); 119 | await prizePool.depositTo(wallet.address, amount); 120 | 121 | await yieldSource.mock.redeemToken.withArgs(amount).returns(amount); 122 | await prizePool.withdrawFrom(wallet.address, amount); 123 | 124 | expect(await ticket.balanceOf(wallet.address)).to.equal('0'); 125 | expect(await depositToken.balanceOf(wallet.address)).to.equal(amount); 126 | }); 127 | }); 128 | 129 | describe('token()', async () => { 130 | it('should return the yield source token', async () => { 131 | expect(await prizePool.getToken()).to.equal(depositToken.address); 132 | }); 133 | }); 134 | 135 | describe('canAwardExternal()', async () => { 136 | it('should not allow the prize pool to award its token, as its likely the receipt', async () => { 137 | expect(await prizePool.canAwardExternal(yieldSource.address)).to.equal(false); 138 | }); 139 | 140 | it('should not allow the prize pool to award the deposit token', async () => { 141 | expect(await prizePool.canAwardExternal(depositToken.address)).to.equal(false); 142 | }) 143 | }); 144 | 145 | describe('sweep()', () => { 146 | it('should sweep stray tokens', async () => { 147 | await depositToken.mint(prizePool.address, toWei('100')) 148 | await yieldSource.mock.supplyTokenTo.withArgs(toWei('100'), prizePool.address).returns() 149 | await expect(prizePool.sweep()) 150 | .to.emit(prizePool, 'Swept') 151 | .withArgs(toWei('100')) 152 | }) 153 | 154 | it('should not allow a non-owner to call it', async () => { 155 | await expect(prizePool.connect(wallet2).sweep()).to.be.revertedWith('Ownable/caller-not-owner') 156 | }) 157 | }) 158 | }); 159 | -------------------------------------------------------------------------------- /deploy/deploy.js: -------------------------------------------------------------------------------- 1 | const { deploy1820 } = require('deploy-eip-1820'); 2 | const chalk = require('chalk'); 3 | 4 | function dim() { 5 | if (!process.env.HIDE_DEPLOY_LOG) { 6 | console.log(chalk.dim.call(chalk, ...arguments)); 7 | } 8 | } 9 | 10 | function cyan() { 11 | if (!process.env.HIDE_DEPLOY_LOG) { 12 | console.log(chalk.cyan.call(chalk, ...arguments)); 13 | } 14 | } 15 | 16 | function yellow() { 17 | if (!process.env.HIDE_DEPLOY_LOG) { 18 | console.log(chalk.yellow.call(chalk, ...arguments)); 19 | } 20 | } 21 | 22 | function green() { 23 | if (!process.env.HIDE_DEPLOY_LOG) { 24 | console.log(chalk.green.call(chalk, ...arguments)); 25 | } 26 | } 27 | 28 | function displayResult(name, result) { 29 | if (!result.newlyDeployed) { 30 | yellow(`Re-used existing ${name} at ${result.address}`); 31 | } else { 32 | green(`${name} deployed at ${result.address}`); 33 | } 34 | } 35 | 36 | const chainName = (chainId) => { 37 | switch (chainId) { 38 | case 1: 39 | return 'Mainnet'; 40 | case 3: 41 | return 'Ropsten'; 42 | case 4: 43 | return 'Rinkeby'; 44 | case 5: 45 | return 'Goerli'; 46 | case 42: 47 | return 'Kovan'; 48 | case 56: 49 | return 'Binance Smart Chain'; 50 | case 77: 51 | return 'POA Sokol'; 52 | case 97: 53 | return 'Binance Smart Chain (testnet)'; 54 | case 99: 55 | return 'POA'; 56 | case 100: 57 | return 'xDai'; 58 | case 137: 59 | return 'Matic'; 60 | case 31337: 61 | return 'HardhatEVM'; 62 | case 80001: 63 | return 'Matic (Mumbai)'; 64 | default: 65 | return 'Unknown'; 66 | } 67 | }; 68 | 69 | module.exports = async (hardhat) => { 70 | const { getNamedAccounts, deployments, getChainId, ethers } = hardhat; 71 | const { deploy } = deployments; 72 | 73 | let { deployer } = await getNamedAccounts(); 74 | const chainId = parseInt(await getChainId(), 10); 75 | 76 | // 31337 is unit testing, 1337 is for coverage 77 | const isTestEnvironment = chainId === 31337 || chainId === 1337; 78 | 79 | const signer = await ethers.provider.getSigner(deployer); 80 | 81 | dim('\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); 82 | dim('PoolTogether Pool Contracts - Deploy Script'); 83 | dim('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n'); 84 | 85 | dim(`Network: ${chainName(chainId)} (${isTestEnvironment ? 'local' : 'remote'})`); 86 | dim(`Deployer: ${deployer}`); 87 | 88 | await deploy1820(signer); 89 | 90 | cyan(`\nDeploying RNGServiceStub...`); 91 | const rngServiceResult = await deploy('RNGServiceStub', { 92 | from: deployer, 93 | }); 94 | 95 | displayResult('RNGServiceStub', rngServiceResult); 96 | 97 | yellow('\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); 98 | yellow('CAUTION: Deploying Prize Pool in a front-runnable way!'); 99 | 100 | cyan('\nDeploying MockYieldSource...'); 101 | const mockYieldSourceResult = await deploy('MockYieldSource', { 102 | from: deployer, 103 | args: ['Token', 'TOK', 18], 104 | }); 105 | 106 | displayResult('MockYieldSource', mockYieldSourceResult); 107 | 108 | cyan('\nDeploying YieldSourcePrizePool...'); 109 | const yieldSourcePrizePoolResult = await deploy('YieldSourcePrizePool', { 110 | from: deployer, 111 | args: [deployer, mockYieldSourceResult.address], 112 | }); 113 | 114 | displayResult('YieldSourcePrizePool', yieldSourcePrizePoolResult); 115 | 116 | cyan('\nDeploying Ticket...'); 117 | const ticketResult = await deploy('Ticket', { 118 | from: deployer, 119 | args: ['Ticket', 'TICK', 18, yieldSourcePrizePoolResult.address], 120 | }); 121 | 122 | displayResult('Ticket', ticketResult); 123 | 124 | cyan('\nsetTicket for YieldSourcePrizePool...'); 125 | 126 | const yieldSourcePrizePool = await ethers.getContract('YieldSourcePrizePool'); 127 | 128 | const setTicketResult = await yieldSourcePrizePool.setTicket(ticketResult.address); 129 | 130 | displayResult('setTicket', setTicketResult); 131 | 132 | yellow('\nPrize Pool Setup Complete'); 133 | yellow('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); 134 | 135 | const cardinality = 8; 136 | 137 | cyan('\nDeploying DrawBuffer...'); 138 | const drawBufferResult = await deploy('DrawBuffer', { 139 | from: deployer, 140 | args: [deployer, cardinality], 141 | }); 142 | displayResult('DrawBuffer', drawBufferResult); 143 | 144 | cyan('\nDeploying PrizeDistributionBuffer...'); 145 | const tsunamiDrawSettindsHistoryResult = await deploy('PrizeDistributionBuffer', { 146 | from: deployer, 147 | args: [deployer, cardinality], 148 | }); 149 | displayResult('PrizeDistributionBuffer', tsunamiDrawSettindsHistoryResult); 150 | 151 | const rngTimeout = 3600 152 | 153 | cyan('\nDeploying DrawBeacon...'); 154 | const drawBeaconResult = await deploy('DrawBeacon', { 155 | from: deployer, 156 | args: [ 157 | deployer, 158 | drawBufferResult.address, 159 | rngServiceResult.address, 160 | 1, 161 | parseInt('' + new Date().getTime() / 1000), 162 | 120, // 2 minute intervals 163 | rngTimeout 164 | ], 165 | }); 166 | 167 | displayResult('DrawBeacon', drawBeaconResult); 168 | 169 | cyan('\nSet DrawBeacon as manager for DrawBuffer...'); 170 | const drawBuffer = await ethers.getContract('DrawBuffer'); 171 | await drawBuffer.setManager(drawBeaconResult.address); 172 | green('DrawBeacon manager set!'); 173 | 174 | cyan('\nDeploying DrawCalculator...'); 175 | const drawCalculatorResult = await deploy('DrawCalculator', { 176 | from: deployer, 177 | args: [ 178 | ticketResult.address, 179 | drawBufferResult.address, 180 | tsunamiDrawSettindsHistoryResult.address, 181 | ], 182 | }); 183 | displayResult('DrawCalculator', drawCalculatorResult); 184 | 185 | cyan('\nDeploying PrizeDistributor...'); 186 | const prizeDistributorResult = await deploy('PrizeDistributor', { 187 | from: deployer, 188 | args: [deployer, ticketResult.address, drawCalculatorResult.address], 189 | }); 190 | displayResult('PrizeDistributor', prizeDistributorResult); 191 | 192 | cyan('\nDeploying PrizeSplitStrategy...'); 193 | const prizeSplitStrategyResult = await deploy('PrizeSplitStrategy', { 194 | from: deployer, 195 | args: [deployer, yieldSourcePrizePoolResult.address], 196 | }); 197 | displayResult('PrizeSplitStrategy', prizeSplitStrategyResult); 198 | 199 | cyan('\nConfiguring PrizeSplitStrategy...'); 200 | const prizeSplitStrategy = await ethers.getContract('PrizeSplitStrategy'); 201 | await prizeSplitStrategy.setPrizeSplits([ 202 | { 203 | target: deployer, 204 | percentage: 1000, // 100% 205 | }, 206 | ]); 207 | green( 208 | 'PrizeSplitStrategy Configured', 209 | `\nPrizeReserve: ${deployer} receives 100% of captured interest`, 210 | ); 211 | 212 | cyan('\nConfiguring PrizeSplitStrategy to be YieldSource strategy...'); 213 | await yieldSourcePrizePool.setPrizeStrategy(prizeSplitStrategyResult.address); 214 | 215 | dim('\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); 216 | green('Contract Deployments Complete!'); 217 | dim('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n'); 218 | }; 219 | -------------------------------------------------------------------------------- /contracts/interfaces/IDrawBeacon.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "@pooltogether/pooltogether-rng-contracts/contracts/RNGInterface.sol"; 6 | import "./IDrawBuffer.sol"; 7 | 8 | /** @title IDrawBeacon 9 | * @author PoolTogether Inc Team 10 | * @notice The DrawBeacon interface. 11 | */ 12 | interface IDrawBeacon { 13 | 14 | /// @notice Draw struct created every draw 15 | /// @param winningRandomNumber The random number returned from the RNG service 16 | /// @param drawId The monotonically increasing drawId for each draw 17 | /// @param timestamp Unix timestamp of the draw. Recorded when the draw is created by the DrawBeacon. 18 | /// @param beaconPeriodStartedAt Unix timestamp of when the draw started 19 | /// @param beaconPeriodSeconds Unix timestamp of the beacon draw period for this draw. 20 | struct Draw { 21 | uint256 winningRandomNumber; 22 | uint32 drawId; 23 | uint64 timestamp; 24 | uint64 beaconPeriodStartedAt; 25 | uint32 beaconPeriodSeconds; 26 | } 27 | 28 | /** 29 | * @notice Emit when a new DrawBuffer has been set. 30 | * @param newDrawBuffer The new DrawBuffer address 31 | */ 32 | event DrawBufferUpdated(IDrawBuffer indexed newDrawBuffer); 33 | 34 | /** 35 | * @notice Emit when a draw has opened. 36 | * @param startedAt Start timestamp 37 | */ 38 | event BeaconPeriodStarted(uint64 indexed startedAt); 39 | 40 | /** 41 | * @notice Emit when a draw has started. 42 | * @param rngRequestId draw id 43 | * @param rngLockBlock Block when draw becomes invalid 44 | */ 45 | event DrawStarted(uint32 indexed rngRequestId, uint32 rngLockBlock); 46 | 47 | /** 48 | * @notice Emit when a draw has been cancelled. 49 | * @param rngRequestId draw id 50 | * @param rngLockBlock Block when draw becomes invalid 51 | */ 52 | event DrawCancelled(uint32 indexed rngRequestId, uint32 rngLockBlock); 53 | 54 | /** 55 | * @notice Emit when a draw has been completed. 56 | * @param randomNumber Random number generated from draw 57 | */ 58 | event DrawCompleted(uint256 randomNumber); 59 | 60 | /** 61 | * @notice Emit when a RNG service address is set. 62 | * @param rngService RNG service address 63 | */ 64 | event RngServiceUpdated(RNGInterface indexed rngService); 65 | 66 | /** 67 | * @notice Emit when a draw timeout param is set. 68 | * @param rngTimeout draw timeout param in seconds 69 | */ 70 | event RngTimeoutSet(uint32 rngTimeout); 71 | 72 | /** 73 | * @notice Emit when the drawPeriodSeconds is set. 74 | * @param drawPeriodSeconds Time between draw 75 | */ 76 | event BeaconPeriodSecondsUpdated(uint32 drawPeriodSeconds); 77 | 78 | /** 79 | * @notice Returns the number of seconds remaining until the beacon period can be complete. 80 | * @return The number of seconds remaining until the beacon period can be complete. 81 | */ 82 | function beaconPeriodRemainingSeconds() external view returns (uint64); 83 | 84 | /** 85 | * @notice Returns the timestamp at which the beacon period ends 86 | * @return The timestamp at which the beacon period ends. 87 | */ 88 | function beaconPeriodEndAt() external view returns (uint64); 89 | 90 | /** 91 | * @notice Returns whether a Draw can be started. 92 | * @return True if a Draw can be started, false otherwise. 93 | */ 94 | function canStartDraw() external view returns (bool); 95 | 96 | /** 97 | * @notice Returns whether a Draw can be completed. 98 | * @return True if a Draw can be completed, false otherwise. 99 | */ 100 | function canCompleteDraw() external view returns (bool); 101 | 102 | /** 103 | * @notice Calculates when the next beacon period will start. 104 | * @param time The timestamp to use as the current time 105 | * @return The timestamp at which the next beacon period would start 106 | */ 107 | function calculateNextBeaconPeriodStartTime(uint64 time) external view returns (uint64); 108 | 109 | /** 110 | * @notice Can be called by anyone to cancel the draw request if the RNG has timed out. 111 | */ 112 | function cancelDraw() external; 113 | 114 | /** 115 | * @notice Completes the Draw (RNG) request and pushes a Draw onto DrawBuffer. 116 | */ 117 | function completeDraw() external; 118 | 119 | /** 120 | * @notice Returns the block number that the current RNG request has been locked to. 121 | * @return The block number that the RNG request is locked to 122 | */ 123 | function getLastRngLockBlock() external view returns (uint32); 124 | 125 | /** 126 | * @notice Returns the current RNG Request ID. 127 | * @return The current Request ID 128 | */ 129 | function getLastRngRequestId() external view returns (uint32); 130 | 131 | /** 132 | * @notice Returns whether the beacon period is over 133 | * @return True if the beacon period is over, false otherwise 134 | */ 135 | function isBeaconPeriodOver() external view returns (bool); 136 | 137 | /** 138 | * @notice Returns whether the random number request has completed. 139 | * @return True if a random number request has completed, false otherwise. 140 | */ 141 | function isRngCompleted() external view returns (bool); 142 | 143 | /** 144 | * @notice Returns whether a random number has been requested 145 | * @return True if a random number has been requested, false otherwise. 146 | */ 147 | function isRngRequested() external view returns (bool); 148 | 149 | /** 150 | * @notice Returns whether the random number request has timed out. 151 | * @return True if a random number request has timed out, false otherwise. 152 | */ 153 | function isRngTimedOut() external view returns (bool); 154 | 155 | /** 156 | * @notice Allows the owner to set the beacon period in seconds. 157 | * @param beaconPeriodSeconds The new beacon period in seconds. Must be greater than zero. 158 | */ 159 | function setBeaconPeriodSeconds(uint32 beaconPeriodSeconds) external; 160 | 161 | /** 162 | * @notice Allows the owner to set the RNG request timeout in seconds. This is the time that must elapsed before the RNG request can be cancelled and the pool unlocked. 163 | * @param rngTimeout The RNG request timeout in seconds. 164 | */ 165 | function setRngTimeout(uint32 rngTimeout) external; 166 | 167 | /** 168 | * @notice Sets the RNG service that the Prize Strategy is connected to 169 | * @param rngService The address of the new RNG service interface 170 | */ 171 | function setRngService(RNGInterface rngService) external; 172 | 173 | /** 174 | * @notice Starts the Draw process by starting random number request. The previous beacon period must have ended. 175 | * @dev The RNG-Request-Fee is expected to be held within this contract before calling this function 176 | */ 177 | function startDraw() external; 178 | 179 | /** 180 | * @notice Set global DrawBuffer variable. 181 | * @dev All subsequent Draw requests/completions will be pushed to the new DrawBuffer. 182 | * @param newDrawBuffer DrawBuffer address 183 | * @return DrawBuffer 184 | */ 185 | function setDrawBuffer(IDrawBuffer newDrawBuffer) external returns (IDrawBuffer); 186 | } 187 | -------------------------------------------------------------------------------- /test/features/support/PoolEnv.js: -------------------------------------------------------------------------------- 1 | const hardhat = require('hardhat'); 2 | const { expect } = require('chai'); 3 | 4 | require('../../helpers/chaiMatchers'); 5 | 6 | const { ethers, deployments } = hardhat; 7 | 8 | const { AddressZero } = ethers.constants; 9 | 10 | const debug = require('debug')('pt:PoolEnv.js'); 11 | 12 | const toWei = (val) => ethers.utils.parseEther('' + val); 13 | 14 | function PoolEnv() { 15 | this.overrides = { gasLimit: 9500000 }; 16 | 17 | this.ready = async function () { 18 | await deployments.fixture(); 19 | this.wallets = await ethers.getSigners(); 20 | }; 21 | 22 | this.wallet = async function (id) { 23 | let wallet = this.wallets[id]; 24 | return wallet; 25 | }; 26 | 27 | this.yieldSource = async () => await ethers.getContract('MockYieldSource'); 28 | 29 | this.token = async function (wallet) { 30 | const yieldSource = await this.yieldSource(); 31 | const tokenAddress = await yieldSource.depositToken(); 32 | return ( 33 | await ethers.getContractAt('contracts/test/ERC20Mintable.sol:ERC20Mintable', tokenAddress) 34 | ).connect(wallet); 35 | }; 36 | 37 | this.ticket = async (wallet) => (await ethers.getContract('Ticket')).connect(wallet); 38 | 39 | this.prizePool = async (wallet) => 40 | (await ethers.getContract('YieldSourcePrizePool')).connect(wallet); 41 | 42 | this.drawBeacon = async () => await ethers.getContract('DrawBeacon'); 43 | 44 | this.drawBuffer = async () => await ethers.getContract('DrawBuffer'); 45 | 46 | this.prizeDistributionBuffer = async () => await ethers.getContract('PrizeDistributionBuffer'); 47 | 48 | this.drawCalculator = async () => await ethers.getContract('DrawCalculator'); 49 | 50 | this.prizeDistributor = async (wallet) => (await ethers.getContract('PrizeDistributor')).connect(wallet); 51 | 52 | this.rng = async () => await ethers.getContract('RNGServiceStub'); 53 | 54 | this.buyTickets = async function ({ user, tickets }) { 55 | debug(`Buying tickets...`); 56 | const owner = await this.wallet(0); 57 | let wallet = await this.wallet(user); 58 | 59 | debug('wallet is ', wallet.address); 60 | let token = await this.token(wallet); 61 | let ticket = await this.ticket(wallet); 62 | let prizePool = await this.prizePool(wallet); 63 | 64 | let amount = toWei(tickets); 65 | 66 | let balance = await token.balanceOf(wallet.address); 67 | 68 | if (balance.lt(amount)) { 69 | await token.mint(wallet.address, amount, this.overrides); 70 | } 71 | 72 | await token.approve(prizePool.address, amount, this.overrides); 73 | 74 | debug(`Depositing... (${wallet.address}, ${amount}, ${ticket.address}, ${AddressZero})`); 75 | 76 | await prizePool.depositTo(wallet.address, amount, this.overrides); 77 | 78 | debug(`Bought tickets`); 79 | }; 80 | 81 | this.buyTicketsForPrizeDistributor = async function ({ user, tickets, prizeDistributor }) { 82 | debug(`Buying tickets...`); 83 | const owner = await this.wallet(0); 84 | let wallet = await this.wallet(user); 85 | 86 | debug('wallet is ', wallet.address); 87 | let token = await this.token(wallet); 88 | let ticket = await this.ticket(wallet); 89 | let prizePool = await this.prizePool(wallet); 90 | 91 | let amount = toWei(tickets); 92 | 93 | let balance = await token.balanceOf(wallet.address); 94 | if (balance.lt(amount)) { 95 | await token.mint(wallet.address, amount, this.overrides); 96 | } 97 | 98 | await token.approve(prizePool.address, amount, this.overrides); 99 | 100 | debug(`Depositing... (${wallet.address}, ${amount}, ${ticket.address}, ${AddressZero})`); 101 | 102 | await prizePool.depositTo(wallet.address, amount, this.overrides); 103 | 104 | debug(`Bought tickets`); 105 | ticket.transfer(prizeDistributor, amount); 106 | 107 | debug(`Transfer tickets to prizeDistributor`); 108 | }; 109 | 110 | this.expectUserToHaveTickets = async function ({ user, tickets }) { 111 | let wallet = await this.wallet(user); 112 | let ticket = await this.ticket(wallet); 113 | let amount = toWei(tickets); 114 | expect(await ticket.balanceOf(wallet.address)).to.equalish(amount, '100000000000000000000'); 115 | }; 116 | 117 | this.expectUserToHaveTokens = async function ({ user, tokens }) { 118 | const wallet = await this.wallet(user); 119 | const token = await this.token(wallet); 120 | const amount = toWei(tokens); 121 | const balance = await token.balanceOf(wallet.address); 122 | debug(`expectUserToHaveTokens: ${balance.toString()}`); 123 | expect(balance).to.equal(amount); 124 | }; 125 | 126 | this.claim = async function ({ user, drawId, picks }) { 127 | const wallet = await this.wallet(user); 128 | const prizeDistributor = await this.prizeDistributor(wallet); 129 | const encoder = ethers.utils.defaultAbiCoder; 130 | const pickIndices = encoder.encode(['uint256[][]'], [[picks]]); 131 | await prizeDistributor.claim(wallet.address, [drawId], pickIndices); 132 | }; 133 | 134 | this.withdraw = async function ({ user, tickets }) { 135 | debug(`withdraw: user ${user}, tickets: ${tickets}`); 136 | let wallet = await this.wallet(user); 137 | let ticket = await this.ticket(wallet); 138 | let withdrawalAmount; 139 | 140 | if (!tickets) { 141 | withdrawalAmount = await ticket.balanceOf(wallet.address); 142 | } else { 143 | withdrawalAmount = toWei(tickets); 144 | } 145 | 146 | debug(`Withdrawing ${withdrawalAmount}...`); 147 | let prizePool = await this.prizePool(wallet); 148 | 149 | await prizePool.withdrawFrom(wallet.address, withdrawalAmount); 150 | 151 | debug('done withdraw'); 152 | }; 153 | 154 | this.poolAccrues = async function ({ tickets }) { 155 | debug(`poolAccrues(${tickets})...`); 156 | const yieldSource = await this.yieldSource(); 157 | await yieldSource.yield(toWei(tickets)); 158 | }; 159 | 160 | this.draw = async function ({ randomNumber }) { 161 | const drawBeacon = await this.drawBeacon(); 162 | const remainingTime = (await drawBeacon.beaconPeriodRemainingSeconds()).toNumber(); 163 | await ethers.provider.send('evm_increaseTime', [remainingTime]); 164 | await drawBeacon.startDraw(); 165 | const rng = await this.rng(); 166 | await rng.setRandomNumber(randomNumber); 167 | await drawBeacon.completeDraw(); 168 | }; 169 | 170 | this.expectDrawRandomNumber = async function ({ drawId, randomNumber }) { 171 | const drawBuffer = await this.drawBuffer(); 172 | const draw = await drawBuffer.getDraw(drawId); 173 | debug(`expectDrawRandomNumber draw: `, draw); 174 | expect(draw.winningRandomNumber).to.equal(randomNumber); 175 | }; 176 | 177 | this.pushPrizeDistribution = async function ({ 178 | drawId, 179 | bitRangeSize, 180 | startTimestampOffset, 181 | endTimestampOffset, 182 | matchCardinality, 183 | expiryDuration, 184 | numberOfPicks, 185 | tiers, 186 | prize, 187 | maxPicksPerUser, 188 | }) { 189 | const prizeDistributionBuffer = await this.prizeDistributionBuffer(); 190 | 191 | const prizeDistributions = { 192 | bitRangeSize, 193 | matchCardinality, 194 | expiryDuration, 195 | startTimestampOffset, 196 | endTimestampOffset, 197 | numberOfPicks, 198 | tiers, 199 | prize, 200 | maxPicksPerUser, 201 | }; 202 | 203 | await prizeDistributionBuffer.pushPrizeDistribution(drawId, prizeDistributions); 204 | }; 205 | } 206 | 207 | module.exports = { 208 | PoolEnv, 209 | }; 210 | -------------------------------------------------------------------------------- /contracts/interfaces/ITicket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.6; 4 | 5 | import "../libraries/TwabLib.sol"; 6 | import "./IControlledToken.sol"; 7 | 8 | interface ITicket is IControlledToken { 9 | /** 10 | * @notice A struct containing details for an Account. 11 | * @param balance The current balance for an Account. 12 | * @param nextTwabIndex The next available index to store a new twab. 13 | * @param cardinality The number of recorded twabs (plus one!). 14 | */ 15 | struct AccountDetails { 16 | uint224 balance; 17 | uint16 nextTwabIndex; 18 | uint16 cardinality; 19 | } 20 | 21 | /** 22 | * @notice Combines account details with their twab history. 23 | * @param details The account details. 24 | * @param twabs The history of twabs for this account. 25 | */ 26 | struct Account { 27 | AccountDetails details; 28 | ObservationLib.Observation[65535] twabs; 29 | } 30 | 31 | /** 32 | * @notice Emitted when TWAB balance has been delegated to another user. 33 | * @param delegator Address of the delegator. 34 | * @param delegate Address of the delegate. 35 | */ 36 | event Delegated(address indexed delegator, address indexed delegate); 37 | 38 | /** 39 | * @notice Emitted when ticket is initialized. 40 | * @param name Ticket name (eg: PoolTogether Dai Ticket (Compound)). 41 | * @param symbol Ticket symbol (eg: PcDAI). 42 | * @param decimals Ticket decimals. 43 | * @param controller Token controller address. 44 | */ 45 | event TicketInitialized(string name, string symbol, uint8 decimals, address indexed controller); 46 | 47 | /** 48 | * @notice Emitted when a new TWAB has been recorded. 49 | * @param delegate The recipient of the ticket power (may be the same as the user). 50 | * @param newTwab Updated TWAB of a ticket holder after a successful TWAB recording. 51 | */ 52 | event NewUserTwab( 53 | address indexed delegate, 54 | ObservationLib.Observation newTwab 55 | ); 56 | 57 | /** 58 | * @notice Emitted when a new total supply TWAB has been recorded. 59 | * @param newTotalSupplyTwab Updated TWAB of tickets total supply after a successful total supply TWAB recording. 60 | */ 61 | event NewTotalSupplyTwab(ObservationLib.Observation newTotalSupplyTwab); 62 | 63 | /** 64 | * @notice Retrieves the address of the delegate to whom `user` has delegated their tickets. 65 | * @dev Address of the delegate will be the zero address if `user` has not delegated their tickets. 66 | * @param user Address of the delegator. 67 | * @return Address of the delegate. 68 | */ 69 | function delegateOf(address user) external view returns (address); 70 | 71 | /** 72 | * @notice Delegate time-weighted average balances to an alternative address. 73 | * @dev Transfers (including mints) trigger the storage of a TWAB in delegate(s) account, instead of the 74 | targetted sender and/or recipient address(s). 75 | * @dev To reset the delegate, pass the zero address (0x000.000) as `to` parameter. 76 | * @dev Current delegate address should be different from the new delegate address `to`. 77 | * @param to Recipient of delegated TWAB. 78 | */ 79 | function delegate(address to) external; 80 | 81 | /** 82 | * @notice Allows the controller to delegate on a users behalf. 83 | * @param user The user for whom to delegate 84 | * @param delegate The new delegate 85 | */ 86 | function controllerDelegateFor(address user, address delegate) external; 87 | 88 | /** 89 | * @notice Allows a user to delegate via signature 90 | * @param user The user who is delegating 91 | * @param delegate The new delegate 92 | * @param deadline The timestamp by which this must be submitted 93 | * @param v The v portion of the ECDSA sig 94 | * @param r The r portion of the ECDSA sig 95 | * @param s The s portion of the ECDSA sig 96 | */ 97 | function delegateWithSignature( 98 | address user, 99 | address delegate, 100 | uint256 deadline, 101 | uint8 v, 102 | bytes32 r, 103 | bytes32 s 104 | ) external; 105 | 106 | /** 107 | * @notice Gets a users twab context. This is a struct with their balance, next twab index, and cardinality. 108 | * @param user The user for whom to fetch the TWAB context. 109 | * @return The TWAB context, which includes { balance, nextTwabIndex, cardinality } 110 | */ 111 | function getAccountDetails(address user) external view returns (TwabLib.AccountDetails memory); 112 | 113 | /** 114 | * @notice Gets the TWAB at a specific index for a user. 115 | * @param user The user for whom to fetch the TWAB. 116 | * @param index The index of the TWAB to fetch. 117 | * @return The TWAB, which includes the twab amount and the timestamp. 118 | */ 119 | function getTwab(address user, uint16 index) 120 | external 121 | view 122 | returns (ObservationLib.Observation memory); 123 | 124 | /** 125 | * @notice Retrieves `user` TWAB balance. 126 | * @param user Address of the user whose TWAB is being fetched. 127 | * @param timestamp Timestamp at which we want to retrieve the TWAB balance. 128 | * @return The TWAB balance at the given timestamp. 129 | */ 130 | function getBalanceAt(address user, uint64 timestamp) external view returns (uint256); 131 | 132 | /** 133 | * @notice Retrieves `user` TWAB balances. 134 | * @param user Address of the user whose TWABs are being fetched. 135 | * @param timestamps Timestamps range at which we want to retrieve the TWAB balances. 136 | * @return `user` TWAB balances. 137 | */ 138 | function getBalancesAt(address user, uint64[] calldata timestamps) 139 | external 140 | view 141 | returns (uint256[] memory); 142 | 143 | /** 144 | * @notice Retrieves the average balance held by a user for a given time frame. 145 | * @param user The user whose balance is checked. 146 | * @param startTime The start time of the time frame. 147 | * @param endTime The end time of the time frame. 148 | * @return The average balance that the user held during the time frame. 149 | */ 150 | function getAverageBalanceBetween( 151 | address user, 152 | uint64 startTime, 153 | uint64 endTime 154 | ) external view returns (uint256); 155 | 156 | /** 157 | * @notice Retrieves the average balances held by a user for a given time frame. 158 | * @param user The user whose balance is checked. 159 | * @param startTimes The start time of the time frame. 160 | * @param endTimes The end time of the time frame. 161 | * @return The average balance that the user held during the time frame. 162 | */ 163 | function getAverageBalancesBetween( 164 | address user, 165 | uint64[] calldata startTimes, 166 | uint64[] calldata endTimes 167 | ) external view returns (uint256[] memory); 168 | 169 | /** 170 | * @notice Retrieves the total supply TWAB balance at the given timestamp. 171 | * @param timestamp Timestamp at which we want to retrieve the total supply TWAB balance. 172 | * @return The total supply TWAB balance at the given timestamp. 173 | */ 174 | function getTotalSupplyAt(uint64 timestamp) external view returns (uint256); 175 | 176 | /** 177 | * @notice Retrieves the total supply TWAB balance between the given timestamps range. 178 | * @param timestamps Timestamps range at which we want to retrieve the total supply TWAB balance. 179 | * @return Total supply TWAB balances. 180 | */ 181 | function getTotalSuppliesAt(uint64[] calldata timestamps) 182 | external 183 | view 184 | returns (uint256[] memory); 185 | 186 | /** 187 | * @notice Retrieves the average total supply balance for a set of given time frames. 188 | * @param startTimes Array of start times. 189 | * @param endTimes Array of end times. 190 | * @return The average total supplies held during the time frame. 191 | */ 192 | function getAverageTotalSuppliesBetween( 193 | uint64[] calldata startTimes, 194 | uint64[] calldata endTimes 195 | ) external view returns (uint256[] memory); 196 | } 197 | --------------------------------------------------------------------------------