├── .nvmrc ├── app ├── .nvmrc ├── .browserslistrc ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ ├── logo.png │ │ ├── logo.svg │ │ └── doubledice-usdc-logo.svg │ ├── shims-vue.d.ts │ ├── config.ts │ ├── main.ts │ ├── components │ │ ├── NewOutcomesComponent.vue │ │ ├── CategoriesComponent.vue │ │ ├── Timeline.vue │ │ ├── NewResultSourcesComponent.vue │ │ ├── NewOpponentsComponent.vue │ │ └── OutcomeComponent.vue │ ├── utils.ts │ └── mm.ts ├── .editorconfig ├── .gitignore ├── apollo.config.js ├── README.md ├── .eslintrc.js ├── tsconfig.json └── package.json ├── .gitignore ├── platform ├── .nvmrc ├── .eslintignore ├── .solhintignore ├── lib │ ├── metadata.ts │ ├── index.ts │ ├── room-event-info │ │ ├── client.ts │ │ ├── common.ts │ │ └── common.spec.ts │ ├── helpers │ │ └── sol-enums.ts │ ├── contracts.ts │ └── graph.ts ├── graphql │ ├── schema.preamble.graphql │ └── codegen.yml ├── .mocharc.json ├── subgraph │ ├── assemblyscript │ │ ├── env.template.ts │ │ ├── tsconfig.json │ │ ├── utils.ts │ │ ├── constants.ts │ │ ├── metadata.ts │ │ └── entities.ts │ ├── subgraph.template.yaml │ └── schema.graphql ├── .gitignore ├── helpers │ ├── index.ts │ ├── evm.ts │ ├── utils.ts │ ├── contract-helpers.ts │ └── deployment.ts ├── .vscode │ └── settings.json ├── contracts │ ├── dummy │ │ ├── DummyUSDCoin.sol │ │ ├── DummyWrappedBTC.sol │ │ └── DummyERC20.sol │ ├── ExtraStorageGap.sol │ ├── mock │ │ ├── enum-wrapper │ │ │ ├── VirtualFloorResolutionTypeWrapper.sol │ │ │ ├── ResolutionStateWrapper.sol │ │ │ ├── ResultUpdateActionWrapper.sol │ │ │ └── VirtualFloorComputedStateWrapper.sol │ │ └── FixedPointTypesMock.sol │ ├── MultipleInheritanceOptimization.sol │ ├── library │ │ ├── Utils.sol │ │ ├── ERC1155TokenIds.sol │ │ ├── VirtualFloorCreationParamsUtils.sol │ │ ├── FixedPointTypes.sol │ │ └── VirtualFloors.sol │ ├── example │ │ └── SimpleOracle.sol │ ├── DoubleDice.sol │ ├── helper │ │ └── GraphHelper.sol │ ├── CreationQuotas.sol │ ├── VirtualFloorMetadataValidator.sol │ └── interface │ │ └── IDoubleDice.sol ├── .solhint.json ├── tsconfig.json ├── .eslintrc.js ├── scripts │ ├── graph-codegen.sh │ ├── decode-doubledice-custom-error.ts │ ├── start.sh │ ├── upgrade.ts │ ├── deploy.ts │ └── create-test-vf.ts ├── docs │ ├── ENTITES.md │ └── STATE.md ├── .env.template ├── package.json ├── package.json.lerna_backup ├── .env.local ├── docker-compose.yml ├── hardhat.config.ts ├── CHANGELOG.md ├── test │ ├── sol-enums.test.ts │ ├── FixedPointTypes.test.ts │ ├── GraphHelper.test.ts │ └── unit-test │ │ ├── 0-CreateVirtualFloor.test.ts │ │ └── 4-FeeRelated.test.ts └── gas-report.txt ├── server ├── netlify │ └── functions │ │ └── erc1155-token-metadata │ │ ├── README.md │ │ └── index.ts ├── README.md ├── netlify.toml ├── package.json ├── package.json.lerna_backup ├── .gitignore ├── .eslintrc.js └── tsconfig.json ├── lerna.json └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.1 2 | -------------------------------------------------------------------------------- /app/.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /platform/.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.1 2 | -------------------------------------------------------------------------------- /platform/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | generated 3 | -------------------------------------------------------------------------------- /app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /platform/.solhintignore: -------------------------------------------------------------------------------- 1 | ForkedERC1155UpgradeableV4_5_2.sol 2 | -------------------------------------------------------------------------------- /platform/lib/metadata.ts: -------------------------------------------------------------------------------- 1 | export * from './room-event-info/client'; -------------------------------------------------------------------------------- /server/netlify/functions/erc1155-token-metadata/README.md: -------------------------------------------------------------------------------- 1 | Coming soon… 2 | -------------------------------------------------------------------------------- /platform/graphql/schema.preamble.graphql: -------------------------------------------------------------------------------- 1 | scalar BigDecimal 2 | scalar BigInt 3 | scalar Bytes 4 | -------------------------------------------------------------------------------- /platform/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "ts-node/register/files", 3 | "timeout": 20000 4 | } -------------------------------------------------------------------------------- /app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Topten1004/DoubleDice-Platform/HEAD/app/public/favicon.ico -------------------------------------------------------------------------------- /app/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Topten1004/DoubleDice-Platform/HEAD/app/src/assets/logo.png -------------------------------------------------------------------------------- /platform/subgraph/assemblyscript/env.template.ts: -------------------------------------------------------------------------------- 1 | export const GRAPH_HELPER_ADDRESS = '${GRAPH_HELPER_ADDRESS}'; 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "platform", 4 | "app", 5 | "server" 6 | ], 7 | "version": "0.1.0" 8 | } -------------------------------------------------------------------------------- /platform/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * as contracts from './contracts'; 2 | export * as graph from './graph'; 3 | export * as metadata from './metadata'; 4 | -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## Project Setup 2 | 3 | npm install 4 | 5 | npm run dev 6 | 7 | Visit http://localhost:8888/api/metadata to test this endpoint 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /platform/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cache/ 3 | artifacts/ 4 | typechain-types/ 5 | generated/ 6 | build/ 7 | .env 8 | .env.* 9 | !.env.local 10 | !.env.template 11 | 12 | coverage.json 13 | coverage/ 14 | -------------------------------------------------------------------------------- /platform/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | export * from './contract-helpers'; 3 | export * from './deployment'; 4 | export * from './evm'; 5 | export * from './utils'; 6 | 7 | -------------------------------------------------------------------------------- /platform/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "mochaExplorer.files": "test/**/*.{js,ts}", 6 | "solidity.compileUsingRemoteVersion": "v0.8.12+commit.f00d7308", 7 | "solidity.formatter": "none" 8 | } -------------------------------------------------------------------------------- /platform/subgraph/assemblyscript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "assemblyscript/std/assembly.json", 3 | "files": [ 4 | "./constants.ts", 5 | "./entities.ts", 6 | "./env.template.ts", 7 | "./mapping.ts", 8 | "./metadata.ts", 9 | "./utils.ts", 10 | ] 11 | } -------------------------------------------------------------------------------- /platform/contracts/dummy/DummyUSDCoin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "./DummyERC20.sol"; 6 | 7 | // solhint-disable-next-line no-empty-blocks 8 | contract DummyUSDCoin is DummyERC20("USD Coin (Dummy)", "USDC", 6) { 9 | } 10 | -------------------------------------------------------------------------------- /platform/contracts/dummy/DummyWrappedBTC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "./DummyERC20.sol"; 6 | 7 | // solhint-disable-next-line no-empty-blocks 8 | contract DummyWrappedBTC is DummyERC20("Wrapped BTC (Dummy)", "WBTC", 8) { 9 | } 10 | -------------------------------------------------------------------------------- /server/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | command = "echo No build command" 4 | node_bundler = "esbuild" 5 | 6 | [dev] 7 | publish = "public" 8 | node_bundler = "esbuild" 9 | 10 | [[redirects]] 11 | from = "/api/*" 12 | to = "/.netlify/functions/:splat" 13 | status = 200 14 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doubledice/server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "ISC", 6 | "dependencies": { 7 | "@netlify/functions": "0.10.0", 8 | "graphql": "16.2.0", 9 | "graphql-request": "3.7.0" 10 | }, 11 | "devDependencies": { 12 | "netlify-cli": "8.11.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | src/generated 26 | -------------------------------------------------------------------------------- /platform/.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": [ 5 | "error", 6 | "0.8.12" 7 | ], 8 | "func-visibility": [ 9 | "error", 10 | { 11 | "ignoreConstructors": true 12 | } 13 | ], 14 | "var-name-mixedcase": "off", 15 | "func-name-mixedcase": "off" 16 | } 17 | } -------------------------------------------------------------------------------- /platform/contracts/ExtraStorageGap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | contract ExtraStorageGap { 6 | 7 | /// @dev Reserve an extra gap, in case we want to extend a new (e.g.) OpenZeppelin base contract 8 | /// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 9 | uint256[200] private __gap; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /platform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "moduleResolution": "Node", 5 | "esModuleInterop": true, 6 | "strictNullChecks": true 7 | }, 8 | "include": [ 9 | "./lib", 10 | "./scripts", 11 | "./test", 12 | "./helpers", 13 | "./src", 14 | ], 15 | "files": [ 16 | "./hardhat.config.ts" 17 | ], 18 | } -------------------------------------------------------------------------------- /app/apollo.config.js: -------------------------------------------------------------------------------- 1 | // apollo.config.js 2 | module.exports = { 3 | client: { 4 | service: { 5 | name: 'my-app', 6 | // URL to the GraphQL API 7 | url: 'http://127.0.0.1:8000/subgraphs/name/doubledice-com/doubledice-platform' 8 | }, 9 | // Files processed by the extension 10 | includes: [ 11 | 'src/**/*.vue', 12 | 'src/**/*.js', 13 | 'src/**/*.ts' 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /platform/graphql/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "./graphql/generated/schema.graphql" 3 | generates: 4 | ./lib/generated/graphql.ts: 5 | plugins: 6 | - typescript 7 | config: 8 | namingConvention: 9 | # see https://www.graphql-code-generator.com/docs/config-reference/naming-convention 10 | enumValues: keep 11 | scalars: 12 | BigDecimal: string 13 | BigInt: string 14 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # app 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /app/src/config.ts: -------------------------------------------------------------------------------- 1 | export const CHAIN_ID = 1337 2 | export const GRAPHQL_QUERIES_URL = 'http://127.0.0.1:8000/subgraphs/name/doubledice-com/doubledice-platform' 3 | export const POLL_INTERVAL_SECONDS = 1 4 | export const MAIN_CONTRACT_ADDRESS = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' 5 | export const USER_ACCOUNT = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' 6 | export const PROVIDER_URL = 'http://localhost:8545' 7 | export const SHOW_VF_JSON_COL = false 8 | -------------------------------------------------------------------------------- /platform/contracts/mock/enum-wrapper/VirtualFloorResolutionTypeWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../../interface/IDoubleDice.sol"; 6 | 7 | contract VirtualFloorResolutionTypeWrapper { 8 | VirtualFloorResolutionType constant public NoWinners = VirtualFloorResolutionType.NoWinners; 9 | VirtualFloorResolutionType constant public Winners = VirtualFloorResolutionType.Winners; 10 | } 11 | -------------------------------------------------------------------------------- /platform/subgraph/assemblyscript/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | // Note: Despite the .ts file extension, this is AssemblyScript not TypeScript! 3 | 4 | import { BigDecimal, BigInt } from '@graphprotocol/graph-ts'; 5 | 6 | export const toDecimal = (wei: BigInt): BigDecimal => wei.divDecimal(new BigDecimal(BigInt.fromU32(10).pow(18))); 7 | 8 | export const paymentTokenAmountToBigDecimal = (wei: BigInt, decimals: i32): BigDecimal => wei.divDecimal(new BigDecimal(BigInt.fromU32(10).pow(u8(decimals)))); 9 | -------------------------------------------------------------------------------- /platform/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | rules: { 12 | quotes: ['error', 'single'], 13 | semi: ['error', 'always'], 14 | 'no-multiple-empty-lines': 'error', 15 | indent: ['error', 2], 16 | 'quote-props': ['error', 'as-needed'], 17 | }, 18 | env: { 19 | node: true 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /server/package.json.lerna_backup: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doubledice/server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "lint:fix": "eslint --fix .", 7 | "dev": "netlify dev", 8 | "test": "ts-mocha --type-check src/**/*.spec.ts" 9 | }, 10 | "license": "ISC", 11 | "dependencies": { 12 | "@doubledice/platform": "0.1.0", 13 | "@netlify/functions": "0.10.0", 14 | "graphql-request": "3.7.0", 15 | "graphql": "16.2.0" 16 | }, 17 | "devDependencies": { 18 | "netlify-cli": "8.11.0" 19 | } 20 | } -------------------------------------------------------------------------------- /platform/contracts/dummy/DummyERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 6 | 7 | contract DummyERC20 is ERC20PresetMinterPauser { 8 | 9 | uint8 immutable private _decimals; 10 | 11 | constructor(string memory name, string memory symbol, uint8 decimals_) ERC20PresetMinterPauser(name, symbol) { 12 | _decimals = decimals_; 13 | } 14 | 15 | function decimals() public view override returns (uint8) { 16 | return _decimals; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # netlify 12 | /.netlify/ 13 | 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | 40 | 41 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | rules: { 12 | quotes: ['error', 'single'], 13 | semi: ['error', 'always'], 14 | 'no-multiple-empty-lines': 'error', 15 | '@typescript-eslint/member-delimiter-style': 'error', 16 | 'quote-props': ['error', 'as-needed'], 17 | 'comma-dangle': ['error', 'always-multiline'], 18 | }, 19 | env: { 20 | node: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /platform/contracts/mock/enum-wrapper/ResolutionStateWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../../ChallengeableCreatorOracle.sol"; 6 | 7 | contract ResolutionStateWrapper { 8 | ResolutionState constant public None = ResolutionState.None; 9 | ResolutionState constant public Set = ResolutionState.Set; 10 | ResolutionState constant public Challenged = ResolutionState.Challenged; 11 | ResolutionState constant public ChallengeCancelled = ResolutionState.ChallengeCancelled; 12 | ResolutionState constant public Complete = ResolutionState.Complete; 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doubledice", 3 | "private": true, 4 | "devDependencies": { 5 | "@types/chai": "4.3.0", 6 | "@types/chai-subset": "1.3.3", 7 | "@types/expect": "24.3.0", 8 | "@types/mocha": "9.1.0", 9 | "@types/node": "16.11.26", 10 | "@typescript-eslint/eslint-plugin": "5.15.0", 11 | "@typescript-eslint/parser": "5.15.0", 12 | "chai": "4.3.6", 13 | "chai-subset": "1.6.0", 14 | "eslint": "8.11.0", 15 | "lerna": "4.0.0", 16 | "mocha": "9.2.2", 17 | "rimraf": "3.0.2", 18 | "ts-mocha": "9.0.2", 19 | "ts-node": "10.7.0", 20 | "typescript": "4.6.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /platform/scripts/graph-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | npx hardhat compile # to generate ./generated/abi/DoubleDice.json 6 | mkdir --verbose --parents ./subgraph/generated 7 | rm --verbose --force ./subgraph/generated/* 8 | cat ./generated/abi/DoubleDice.json | jq 'del(.[] | select(.type == "error"))' > ./subgraph/generated/DoubleDice.no-custom-errors.json 9 | npx envsub --protect --env-file .env ./subgraph/subgraph.template.yaml ./subgraph/generated/subgraph.yaml 10 | npx envsub --protect --env-file .env ./subgraph/assemblyscript/env.template.ts ./subgraph/generated/env.ts 11 | npx graph codegen ./subgraph/generated/subgraph.yaml 12 | -------------------------------------------------------------------------------- /platform/contracts/MultipleInheritanceOptimization.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | contract MultipleInheritanceOptimization { 6 | 7 | bool private _rootInitialized; 8 | 9 | modifier multipleInheritanceRootInitializer() { 10 | if (!_rootInitialized) { 11 | _rootInitialized = true; 12 | _; 13 | } 14 | } 15 | 16 | modifier multipleInheritanceLeafInitializer() { 17 | _; 18 | _rootInitialized = false; 19 | } 20 | 21 | /// @dev See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 22 | uint256[50] private __gap; 23 | } 24 | -------------------------------------------------------------------------------- /platform/subgraph/assemblyscript/constants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | // Note: Despite the .ts file extension, this is AssemblyScript not TypeScript! 3 | 4 | import { BigInt } from '@graphprotocol/graph-ts'; 5 | 6 | const ONE_HOUR = BigInt.fromU32(60 * 60); 7 | 8 | // ToDo: Emit these values per-VF on VirtualFloorCreation event 9 | export const SET_WINDOW_DURATION = ONE_HOUR; 10 | export const CHALLENGE_WINDOW_DURATION = ONE_HOUR; 11 | 12 | /** 13 | * An entity id used for singleton aggregate entities, 14 | * inspired by https://github.com/centrehq/usdc-subgraph/blob/master/src/mapping.ts 15 | */ 16 | export const SINGLETON_AGGREGATE_ENTITY_ID = 'singleton'; 17 | -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | '@vue/standard', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'space-before-function-paren': ['error', 'never'], 18 | semi: ['error', 'never'], 19 | 'no-multiple-empty-lines': 'error', 20 | "space-before-function-paren": ["error", { 21 | "anonymous": "always", 22 | "named": "never", 23 | "asyncArrow": "always" 24 | }], 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /platform/contracts/library/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | error TooLargeForUint192(uint256 value); 6 | 7 | library Utils { 8 | 9 | function toUint192(uint256 value) internal pure returns (uint192) { 10 | if (!(value <= type(uint192).max)) revert TooLargeForUint192(value); 11 | return uint192(value); 12 | } 13 | 14 | function isEmpty(string memory value) internal pure returns (bool) { 15 | return bytes(value).length == 0; 16 | } 17 | 18 | function add(uint256 a, int256 b) internal pure returns (uint256) { 19 | if (b >= 0) { 20 | return a + uint256(b); 21 | } else { 22 | return a - uint256(-b); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /platform/scripts/decode-doubledice-custom-error.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config'; 2 | import { decodeDoubleDiceCustomErrorData } from '../lib/contracts'; 3 | 4 | // E.g. 5 | // 6 | // $ npx hardhat decode-dd-error 0xf1df2bd0 7 | // Error: CreationQuotaExceeded() 8 | // Data: [] 9 | 10 | task('decode-dd-error', 'Decode DoubleDice contract custom error data') 11 | .addPositionalParam('data', 'Error data 0x...') 12 | .setAction(async ({ data }) => { 13 | const decoded = decodeDoubleDiceCustomErrorData(data); 14 | if (decoded) { 15 | const { name, formattedArgs } = decoded; 16 | console.log(`${name}(${formattedArgs})`); 17 | } else { 18 | console.log(`No matching custom error found on the DoubleDice contract for data: ${data}`); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /platform/contracts/example/SimpleOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../BaseDoubleDice.sol"; 6 | 7 | contract SimpleOracle is BaseDoubleDice { 8 | 9 | function __SimpleOracle_init(BaseDoubleDiceInitParams calldata params) internal onlyInitializing { 10 | __BaseDoubleDice_init(params); 11 | } 12 | 13 | function resolve(uint256 vfId, uint8 winningOutcomeIndex) external { 14 | address creator = getVirtualFloorCreator(vfId); 15 | if (!(_msgSender() == creator)) revert UnauthorizedMsgSender(); 16 | _resolve(vfId, winningOutcomeIndex, creator); 17 | } 18 | 19 | /// @dev See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 20 | uint256[50] private __gap; 21 | } 22 | -------------------------------------------------------------------------------- /platform/contracts/mock/enum-wrapper/ResultUpdateActionWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../../ChallengeableCreatorOracle.sol"; 6 | 7 | contract ResultUpdateActionWrapper { 8 | ResultUpdateAction constant public AdminFinalizedUnsetResult = ResultUpdateAction.AdminFinalizedUnsetResult; 9 | ResultUpdateAction constant public CreatorSetResult = ResultUpdateAction.CreatorSetResult; 10 | ResultUpdateAction constant public SomeoneConfirmedUnchallengedResult = ResultUpdateAction.SomeoneConfirmedUnchallengedResult; 11 | ResultUpdateAction constant public SomeoneChallengedSetResult = ResultUpdateAction.SomeoneChallengedSetResult; 12 | ResultUpdateAction constant public AdminFinalizedChallenge = ResultUpdateAction.AdminFinalizedChallenge; 13 | } 14 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } -------------------------------------------------------------------------------- /platform/lib/room-event-info/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { RoomEventInfo } from '../contracts'; 3 | import { validateRoomEventInfo } from './common'; 4 | 5 | export class RoomEventInfoClient { 6 | 7 | constructor(private origin: string = 'http://localhost:8888') { 8 | } 9 | 10 | async submitRoomEventInfo(roomEventInfo: RoomEventInfo): Promise { 11 | 12 | if (!validateRoomEventInfo(roomEventInfo)) { 13 | throw new Error(JSON.stringify(validateRoomEventInfo.errors)); 14 | } 15 | 16 | const rsp = await axios.post(`${this.origin}/api/room-event-info?action=submit`, roomEventInfo); 17 | 18 | if (rsp.status === 200) { 19 | const { metadataHash } = rsp.data as { metadataHash: string }; 20 | return metadataHash; 21 | } else { 22 | const { errors } = rsp.data as { errors: any[] }; 23 | console.error(errors); 24 | throw new Error(JSON.stringify(errors)); 25 | } 26 | } 27 | 28 | } 29 | 30 | export { validateRoomEventInfo }; 31 | -------------------------------------------------------------------------------- /platform/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if nc -z localhost 5432; then 6 | echo 'Cannot start if there is a service running on port 5432' 7 | echo 'Maybe run: "sudo service postgresql stop" or "npm stop"' 8 | exit 1 9 | fi 10 | 11 | docker-compose up & 12 | 13 | # Wait for Ganache to respond to a test-request, rather than simply waiting for port 8545 to be up 14 | while ! curl -H "Content-Type: application/json" -X POST --data '{"id":0,"jsonrpc":"2.0","method":"web3_clientVersion","params":[]}' http://localhost:8545; do 15 | echo "Waiting for Ganache on localhost:8545 to respond to a test-request..." 16 | sleep 0.5 17 | done 18 | 19 | npm run contracts:deploy:local 20 | 21 | npm run graph:all:local 22 | 23 | docker exec platform_ipfs_1 ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://localhost:8080"]' 24 | docker exec platform_ipfs_1 ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]' 25 | docker restart platform_ipfs_1 26 | -------------------------------------------------------------------------------- /platform/docs/ENTITES.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | erDiagram 3 | VirtualFloor ||--|{ Outcome: has 4 | 5 | Outcome ||--o{ OutcomeTimeslot: has 6 | 7 | Outcome ||--o{ UserOutcome: has 8 | Outcome ||--o{ UserOutcomeTimeslot: has 9 | 10 | User ||--o{ UserOutcome: has 11 | User ||--o{ UserOutcomeTimeslot: has 12 | 13 | UserOutcome ||--o{ UserOutcomeTimeslot: has 14 | OutcomeTimeslot ||--o{ UserOutcomeTimeslot: has 15 | 16 | VirtualFloor { 17 | BigInt virtualFloorId 18 | BigDecimal totalSupply 19 | } 20 | Outcome { 21 | Int outcomeIndex 22 | BigDecimal totalSupply 23 | BigDecimal totalWeightedSupply 24 | } 25 | OutcomeTimeslot { 26 | BigInt timeslot 27 | BigInt tokenId 28 | BigDecimal beta 29 | BigDecimal totalSupply 30 | } 31 | UserOutcome { 32 | BigDecimal totalBalance 33 | BigDecimal totalWeightedBalance 34 | } 35 | UserOutcomeTimeslot { 36 | BigDecimal balance 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /platform/subgraph/assemblyscript/metadata.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | Bytes, log 4 | } from '@graphprotocol/graph-ts'; 5 | import { 6 | VirtualFloorCreationMetadataStruct 7 | } from '../../generated/DoubleDice/DoubleDice'; 8 | import { 9 | GraphHelper, 10 | GraphHelper__decodeVirtualFloorMetadataV1ResultDecodedStruct 11 | } from '../../generated/DoubleDice/GraphHelper'; 12 | import { GRAPH_HELPER_ADDRESS } from '../generated/env'; 13 | 14 | export function decodeMetadata(wrappedMetadata: VirtualFloorCreationMetadataStruct): GraphHelper__decodeVirtualFloorMetadataV1ResultDecodedStruct { 15 | if (wrappedMetadata.version == Bytes.fromHexString('0x0000000000000000000000000000000000000000000000000000000000000001')) { 16 | const helper = GraphHelper.bind(Address.fromString(GRAPH_HELPER_ADDRESS)); 17 | return helper.decodeVirtualFloorMetadataV1(wrappedMetadata.data); 18 | } else { 19 | log.critical('Metadata version {} not supported', [wrappedMetadata.version.toHex()]); 20 | throw new Error(`Error: Metadata version ${wrappedMetadata.version.toHex()} not supported`); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /platform/scripts/upgrade.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { ethers } from 'hardhat'; 3 | import { 4 | upgradeDoubleDice 5 | } from '../helpers'; 6 | 7 | const { 8 | CHAIN_ID, 9 | OWNER_ADDRESS, 10 | DOUBLEDICE_CONTRACT_ADDRESS, 11 | DOUBLEDICE_PROXY_ADMIN_ADDRESS, 12 | } = process.env; 13 | 14 | async function main() { 15 | 16 | assert(CHAIN_ID); 17 | assert(OWNER_ADDRESS); 18 | assert(DOUBLEDICE_CONTRACT_ADDRESS); 19 | assert(DOUBLEDICE_PROXY_ADMIN_ADDRESS); 20 | 21 | const { chainId } = await ethers.provider.getNetwork(); 22 | assert(parseInt(CHAIN_ID) === chainId, `${CHAIN_ID} !== ${chainId}; wrong .env config?`); 23 | 24 | const deployer = await ethers.getSigner(OWNER_ADDRESS); 25 | 26 | await upgradeDoubleDice({ 27 | deployer: deployer, 28 | deployArgs: [], 29 | deployedTransparentUpgradeableProxyAddress: DOUBLEDICE_CONTRACT_ADDRESS, 30 | deployedProxyAdminAddress: DOUBLEDICE_PROXY_ADMIN_ADDRESS, 31 | }); 32 | 33 | } 34 | 35 | main() 36 | .then(() => process.exit(0)) 37 | .catch(error => { 38 | console.error(error); 39 | process.exit(1); 40 | }); 41 | -------------------------------------------------------------------------------- /platform/.env.template: -------------------------------------------------------------------------------- 1 | # Deployment safety-net 2 | # Required for contract deployment 3 | CHAIN_ID=4 4 | 5 | # Required for contract deployment 6 | PROVIDER_URL= 7 | 8 | # Deployer and initial owner() 9 | # Required for contract deployment 10 | OWNER_ADDRESS= 11 | OWNER_PRIVATE_KEY= 12 | 13 | # Constructor args 14 | # Required for contract deployment 15 | INIT_TOKEN_METADATA_URI_TEMPLATE= 16 | INIT_PLATFORM_FEE_RATE= 17 | INIT_PLATFORM_FEE_BENEFICIARY= 18 | INIT_CONTRACT_URI= 19 | 20 | # Required for Graph deployment 21 | DOUBLEDICE_CONTRACT_NETWORK= 22 | DOUBLEDICE_CONTRACT_ADDRESS= 23 | DOUBLEDICE_CONTRACT_START_BLOCK= 24 | GRAPH_HELPER_ADDRESS= 25 | 26 | # Optional for contract deployment: Deployment of GraphHelper will be skipped if this var is set to true|yes|1 27 | SKIP_DEPLOY_GRAPH_HELPER= 28 | 29 | # Required for contract upgrade 30 | DOUBLEDICE_PROXY_ADMIN_ADDRESS= 31 | 32 | # Optional for contract deployment: If each of these vars are set, 33 | # corresponding ERC-20 contract will not be deployed, 34 | # and DoubleDice contract will be configured with these payment-tokens. 35 | DEPLOYED_USDC_ADDRESS= 36 | DEPLOYED_WBTC_ADDRESS= 37 | -------------------------------------------------------------------------------- /platform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doubledice/platform", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "./lib/index.ts", 6 | "files": [ 7 | "lib/*" 8 | ], 9 | "devDependencies": { 10 | "@assemblyscript/loader": "0.19.23", 11 | "@graphprotocol/graph-cli": "0.26.0", 12 | "@graphprotocol/graph-ts": "0.26.0", 13 | "@graphql-codegen/cli": "2.6.2", 14 | "@graphql-codegen/typescript": "2.4.8", 15 | "@nomiclabs/hardhat-ethers": "2.0.5", 16 | "@nomiclabs/hardhat-waffle": "2.0.3", 17 | "@openzeppelin/contracts": "4.5.0", 18 | "@openzeppelin/contracts-upgradeable": "4.5.2", 19 | "@typechain/ethers-v5": "9.0.0", 20 | "@typechain/hardhat": "5.0.0", 21 | "abi-to-sol": "0.5.2", 22 | "assemblyscript": "0.19.23", 23 | "dotenv": "16.0.0", 24 | "envsub": "4.0.7", 25 | "ethereum-waffle": "3.4.0", 26 | "ethers": "5.6.1", 27 | "ethlint": "1.2.5", 28 | "hardhat": "2.9.1", 29 | "hardhat-abi-exporter": "2.8.0", 30 | "hardhat-gas-reporter": "1.0.8", 31 | "solhint": "3.3.7", 32 | "solidity-coverage": "0.7.20", 33 | "typechain": "7.0.1" 34 | }, 35 | "dependencies": { 36 | "ajv": "8.10.0", 37 | "ajv-formats": "2.1.1", 38 | "axios": "0.26.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /platform/package.json.lerna_backup: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doubledice/platform", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "./lib/index.ts", 6 | "files": [ 7 | "lib/*" 8 | ], 9 | "devDependencies": { 10 | "@assemblyscript/loader": "0.19.23", 11 | "@graphprotocol/graph-cli": "0.26.0", 12 | "@graphprotocol/graph-ts": "0.26.0", 13 | "@graphql-codegen/cli": "2.6.2", 14 | "@graphql-codegen/typescript": "2.4.8", 15 | "@nomiclabs/hardhat-ethers": "2.0.5", 16 | "@nomiclabs/hardhat-waffle": "2.0.3", 17 | "@openzeppelin/contracts": "4.5.0", 18 | "@openzeppelin/contracts-upgradeable": "4.5.2", 19 | "@typechain/ethers-v5": "9.0.0", 20 | "@typechain/hardhat": "5.0.0", 21 | "abi-to-sol": "0.5.2", 22 | "assemblyscript": "0.19.23", 23 | "dotenv": "16.0.0", 24 | "envsub": "4.0.7", 25 | "ethereum-waffle": "3.4.0", 26 | "ethers": "5.6.1", 27 | "ethlint": "1.2.5", 28 | "hardhat": "2.9.1", 29 | "hardhat-abi-exporter": "2.8.0", 30 | "hardhat-gas-reporter": "1.0.8", 31 | "solhint": "3.3.7", 32 | "solidity-coverage": "0.7.20", 33 | "typechain": "7.0.1" 34 | }, 35 | "dependencies": { 36 | "ajv": "8.10.0", 37 | "ajv-formats": "2.1.1", 38 | "axios": "0.26.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /platform/lib/helpers/sol-enums.ts: -------------------------------------------------------------------------------- 1 | // This file doubles as a TypeScript and as an AssemblyScript file 2 | 3 | export enum VirtualFloorResolutionType { 4 | NoWinners, 5 | Winners 6 | } 7 | 8 | export enum VirtualFloorState { 9 | None, 10 | Active_Open_MaybeResolvableNever, // formerly Running 11 | Active_Open_ResolvableLater, // formerly Running 12 | Active_Closed_ResolvableNever, // formerly ClosedUnresolvable 13 | Active_Closed_ResolvableLater, // formerly ClosedPreResolvable 14 | Active_Closed_ResolvableNow, // formerly ClosedResolvable 15 | Claimable_Payouts, // formerly ResolvedWinners 16 | Claimable_Refunds_ResolvedNoWinners, // formerly CancelledResolvedNoWinners 17 | Claimable_Refunds_ResolvableNever, // formerly CancelledUnresolvable 18 | Claimable_Refunds_Flagged // formerly CancelledFlagged 19 | } 20 | 21 | export enum ResultUpdateAction { 22 | AdminFinalizedUnsetResult, 23 | CreatorSetResult, 24 | SomeoneConfirmedUnchallengedResult, 25 | SomeoneChallengedSetResult, 26 | AdminFinalizedChallenge 27 | } 28 | 29 | export enum ResolutionState { 30 | None, 31 | Set, 32 | Challenged, 33 | ChallengeCancelled, 34 | Complete 35 | } 36 | -------------------------------------------------------------------------------- /platform/contracts/mock/enum-wrapper/VirtualFloorComputedStateWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../../interface/IDoubleDice.sol"; 6 | 7 | contract VirtualFloorStateWrapper { 8 | VirtualFloorState constant public None = VirtualFloorState.None; 9 | VirtualFloorState constant public Active_Open_MaybeResolvableNever = VirtualFloorState.Active_Open_MaybeResolvableNever; 10 | VirtualFloorState constant public Active_Open_ResolvableLater = VirtualFloorState.Active_Open_ResolvableLater; 11 | VirtualFloorState constant public Active_Closed_ResolvableNever = VirtualFloorState.Active_Closed_ResolvableNever; 12 | VirtualFloorState constant public Active_Closed_ResolvableLater = VirtualFloorState.Active_Closed_ResolvableLater; 13 | VirtualFloorState constant public Active_Closed_ResolvableNow = VirtualFloorState.Active_Closed_ResolvableNow; 14 | VirtualFloorState constant public Claimable_Payouts = VirtualFloorState.Claimable_Payouts; 15 | VirtualFloorState constant public Claimable_Refunds_ResolvedNoWinners = VirtualFloorState.Claimable_Refunds_ResolvedNoWinners; 16 | VirtualFloorState constant public Claimable_Refunds_ResolvableNever = VirtualFloorState.Claimable_Refunds_ResolvableNever; 17 | VirtualFloorState constant public Claimable_Refunds_Flagged = VirtualFloorState.Claimable_Refunds_Flagged; 18 | } 19 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@doubledice/app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@apollo/client": "^3.5.7", 12 | "@doubledice/platform": "0.1.0", 13 | "@metamask/detect-provider": "^1.2.0", 14 | "@metamask/onboarding": "^1.0.1", 15 | "@metamask/providers": "^8.1.1", 16 | "@vue/apollo-option": "^4.0.0-alpha.16", 17 | "axios": "0.25.0", 18 | "bignumber.js": "^9.0.2", 19 | "core-js": "^3.20.3", 20 | "ethers": "^5.5.3", 21 | "graphql-tag": "^2.12.6", 22 | "vue-class-component": "^8.0.0-0", 23 | "vue": "^3.0.0" 24 | }, 25 | "devDependencies": { 26 | "@metamask/types": "^1.0.0", 27 | "@types/bignumber.js": "^5.0.0", 28 | "@vue/cli-plugin-babel": "~4.5.15", 29 | "@vue/cli-plugin-eslint": "~4.5.15", 30 | "@vue/cli-plugin-typescript": "~4.5.15", 31 | "@vue/cli-service": "~4.5.15", 32 | "@vue/compiler-sfc": "^3.2.28", 33 | "@vue/eslint-config-standard": "^5.1.2", 34 | "@vue/eslint-config-typescript": "^7.0.0", 35 | "datauri": "4.1.0", 36 | "eslint-plugin-import": "^2.20.2", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.0.0", 40 | "eslint-plugin-vue": "^7.0.0" 41 | } 42 | } -------------------------------------------------------------------------------- /platform/contracts/DoubleDice.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "./ChallengeableCreatorOracle.sol"; 6 | import "./CreationQuotas.sol"; 7 | import "./VirtualFloorMetadataValidator.sol"; 8 | 9 | contract DoubleDice is 10 | ChallengeableCreatorOracle, 11 | CreationQuotas, 12 | VirtualFloorMetadataValidator 13 | { 14 | 15 | function initialize( 16 | BaseDoubleDiceInitParams calldata params, 17 | IERC20MetadataUpgradeable bondUsdErc20Token_ 18 | ) 19 | external 20 | initializer 21 | multipleInheritanceLeafInitializer 22 | { 23 | __ChallengeableCreatorOracle_init(params, bondUsdErc20Token_); 24 | __VirtualFloorMetadataValidator_init(params); 25 | __CreationQuotas_init(params); 26 | } 27 | 28 | function _onVirtualFloorCreation(VirtualFloorCreationParams calldata params) 29 | internal override(BaseDoubleDice, VirtualFloorMetadataValidator, CreationQuotas) 30 | { 31 | CreationQuotas._onVirtualFloorCreation(params); 32 | VirtualFloorMetadataValidator._onVirtualFloorCreation(params); 33 | } 34 | 35 | function _onVirtualFloorConclusion(uint256 vfId) 36 | internal override(BaseDoubleDice, ChallengeableCreatorOracle, CreationQuotas) 37 | { 38 | ChallengeableCreatorOracle._onVirtualFloorConclusion(vfId); 39 | CreationQuotas._onVirtualFloorConclusion(vfId); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core' 2 | import detectEthereumProvider from '@metamask/detect-provider' 3 | import { createApolloProvider } from '@vue/apollo-option' 4 | import { createApp, h } from 'vue' 5 | import App from './App.vue' 6 | import { GRAPHQL_QUERIES_URL } from './config' 7 | 8 | async function main() { 9 | const ethereumProvider = await detectEthereumProvider() 10 | 11 | // HTTP connection to the API 12 | const httpLink = createHttpLink({ uri: GRAPHQL_QUERIES_URL }) 13 | 14 | // Cache implementation 15 | const cache = new InMemoryCache() 16 | 17 | // Create the apollo client 18 | const apolloClient = new ApolloClient({ 19 | link: httpLink, 20 | cache 21 | }) 22 | 23 | const apolloProvider = createApolloProvider({ 24 | defaultClient: apolloClient 25 | }) 26 | 27 | if (ethereumProvider) { 28 | // From now on, this should always be true: 29 | // provider === window.ethereum 30 | const app = createApp({ 31 | render: () => h(App) 32 | }) 33 | app.use(apolloProvider) 34 | 35 | // app.config.globalProperties.$filters = { 36 | // formatTimestamp(timestamp: string | number): string { 37 | // return new Date(parseInt(timestamp.toString()) * 1000).toISOString().slice(0, 19).replace(/-/g, '\u2011') 38 | // } 39 | // } 40 | 41 | app.mount('#app') 42 | } else { 43 | alert('🦊 Please install MetaMask! 🦊') 44 | } 45 | } 46 | 47 | main() 48 | -------------------------------------------------------------------------------- /platform/.env.local: -------------------------------------------------------------------------------- 1 | # Deployment safety-net 2 | # Required for contract deployment 3 | CHAIN_ID=1337 4 | 5 | # Required for contract deployment 6 | PROVIDER_URL=http://localhost:8545 7 | 8 | # Deployer and initial owner() 9 | # Required for contract deployment 10 | OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 11 | OWNER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 12 | 13 | # Constructor args 14 | # Required for contract deployment 15 | INIT_TOKEN_METADATA_URI_TEMPLATE=http://localhost:8080/token/{id} 16 | INIT_PLATFORM_FEE_RATE=0.2500 17 | INIT_PLATFORM_FEE_BENEFICIARY=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 18 | INIT_CONTRACT_URI=http://localhost:8080/contract-metadata.json 19 | 20 | # Required for Graph deployment 21 | DOUBLEDICE_CONTRACT_NETWORK=mainnet 22 | DOUBLEDICE_CONTRACT_ADDRESS=0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 23 | DOUBLEDICE_CONTRACT_START_BLOCK=0 24 | GRAPH_HELPER_ADDRESS=0x610178dA211FEF7D417bC0e6FeD39F05609AD788 25 | 26 | # Optional for contract deployment: Deployment of GraphHelper will be skipped if this var is set to true|yes|1 27 | SKIP_DEPLOY_GRAPH_HELPER=no 28 | 29 | # Required for contract upgrade 30 | DOUBLEDICE_PROXY_ADMIN_ADDRESS=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 31 | 32 | # Optional for contract deployment: If each of these vars are set, 33 | # corresponding ERC-20 contract will not be deployed, 34 | # and DoubleDice contract will be configured with these payment-tokens. 35 | DEPLOYED_USDC_ADDRESS= 36 | DEPLOYED_WBTC_ADDRESS= 37 | -------------------------------------------------------------------------------- /platform/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | graph-node: 4 | image: graphprotocol/graph-node:v0.25.2 5 | ports: 6 | - '8000:8000' 7 | - '8001:8001' 8 | - '8020:8020' 9 | - '8030:8030' 10 | - '8040:8040' 11 | depends_on: 12 | - ipfs 13 | - postgres 14 | - ganache-cli 15 | environment: 16 | postgres_host: postgres 17 | postgres_user: graph-node 18 | postgres_pass: let-me-in 19 | postgres_db: graph-node 20 | ipfs: 'ipfs:5001' 21 | ethereum: 'mainnet:http://ganache-cli:8545' 22 | GRAPH_LOG: info 23 | # Note: To avoid "ipfs.map is deprecated. Improved support for IPFS will be added in the future" 24 | # ipfs.map works on the free (centralized) thegraph hosted service 25 | GRAPH_ALLOW_NON_DETERMINISTIC_IPFS: 1 26 | ipfs: 27 | image: ipfs/go-ipfs:v0.12.0-rc1 28 | ports: 29 | - '5001:5001' 30 | postgres: 31 | image: postgres 32 | ports: 33 | - '5432:5432' 34 | command: 35 | [ 36 | "postgres", 37 | "-cshared_preload_libraries=pg_stat_statements" 38 | ] 39 | environment: 40 | POSTGRES_USER: graph-node 41 | POSTGRES_PASSWORD: let-me-in 42 | POSTGRES_DB: graph-node 43 | ganache-cli: 44 | image: trufflesuite/ganache-cli:v6.12.2 45 | ports: 46 | - '8545:8545' 47 | entrypoint: 48 | - node 49 | - /app/ganache-core.docker.cli.js 50 | - --mnemonic 51 | - 'test test test test test test test test test test test junk' 52 | - --networkId 53 | - '1337' 54 | - --chainId 55 | - '1337' 56 | - --hostname 57 | - '0.0.0.0' 58 | -------------------------------------------------------------------------------- /app/src/components/NewOutcomesComponent.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 59 | 60 | 62 | -------------------------------------------------------------------------------- /platform/contracts/helper/GraphHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../VirtualFloorMetadataValidator.sol"; 6 | 7 | /// @dev The purpose of this contract is to assist the Graph indexer in abi-decoding an abi-encoded VirtualFloorMetadataV1 structure. 8 | /// In theory the Graph should be able to abi-decode such a structure via the AssemblyScript function 9 | /// [ethereum.decode](https://thegraph.com/docs/en/developer/assemblyscript-api/#encoding-decoding-abi). 10 | /// However this function doesn't seem to handle tuple-arrays correctly, 11 | /// so as the Graph indexer has the ability to call a deployed contract, 12 | /// we work around the limitation by deploying this helper contract which 13 | /// is then used by the Graph to decode metadata. 14 | contract GraphHelper { 15 | 16 | /// @dev This function never needs to be called on the contract, and its sole purpose is to coerce TypeChain 17 | /// into generating a corresponding encodeFunctionData, which can be used to abi-encode a VirtualFloorMetadataV1 18 | /// without ever communicating with the deployed contract. 19 | /// Nevertheless: 20 | /// 1. Rather than on a separate interface, for simplicity it is included on this contract (and unnecessarily deployed) 21 | /// 2. Although it would have sufficed to have an empty implementation, we choose to include it 22 | function encodeVirtualFloorMetadataV1(VirtualFloorMetadataV1 calldata decoded) external pure returns (bytes memory encoded) { 23 | encoded = abi.encode(decoded); 24 | } 25 | 26 | function decodeVirtualFloorMetadataV1(bytes calldata encoded) external pure returns (VirtualFloorMetadataV1 memory decoded) { 27 | (decoded) = abi.decode(encoded, (VirtualFloorMetadataV1)); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/components/CategoriesComponent.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 58 | 59 | 67 | -------------------------------------------------------------------------------- /app/src/components/Timeline.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 56 | 57 | 65 | -------------------------------------------------------------------------------- /platform/contracts/CreationQuotas.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "./BaseDoubleDice.sol"; 6 | import "./library/Utils.sol"; 7 | 8 | error CreationQuotaExceeded(); 9 | 10 | /// @dev Gas-naive implementation 11 | contract CreationQuotas is BaseDoubleDice { 12 | 13 | using Utils for uint256; 14 | 15 | function __CreationQuotas_init(BaseDoubleDiceInitParams calldata params) internal onlyInitializing { 16 | __BaseDoubleDice_init(params); 17 | } 18 | 19 | mapping(address => uint256) public creationQuotas; 20 | 21 | function _onVirtualFloorCreation(VirtualFloorCreationParams calldata params) internal override virtual { 22 | address creator = getVirtualFloorCreator(params.virtualFloorId); 23 | if (creationQuotas[creator] == 0) revert CreationQuotaExceeded(); 24 | unchecked { 25 | creationQuotas[creator] -= 1; 26 | } 27 | } 28 | 29 | function _onVirtualFloorConclusion(uint256 vfId) internal override virtual { 30 | address creator = getVirtualFloorCreator(vfId); 31 | creationQuotas[creator] += 1; 32 | } 33 | 34 | struct QuotaAdjustment { 35 | address creator; 36 | int256 relativeAmount; 37 | } 38 | 39 | event CreationQuotaAdjustments(QuotaAdjustment[] adjustments); 40 | 41 | function adjustCreationQuotas(QuotaAdjustment[] calldata adjustments) 42 | external 43 | onlyRole(OPERATOR_ROLE) 44 | { 45 | for (uint256 i = 0; i < adjustments.length; i++) { 46 | QuotaAdjustment calldata adjustment = adjustments[i]; 47 | creationQuotas[adjustment.creator] = creationQuotas[adjustment.creator].add(adjustment.relativeAmount); 48 | } 49 | emit CreationQuotaAdjustments(adjustments); 50 | } 51 | 52 | /// @dev See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 53 | uint256[50] private __gap; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /platform/helpers/evm.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, providers } from 'ethers'; 2 | import hre from 'hardhat'; 3 | import { toTimestamp } from '.'; 4 | 5 | // Ported from https://github.com/DoubleDice-com/doubledice-token/blob/master/test/lib/utils.ts 6 | export class EvmCheckpoint { 7 | 8 | private readonly provider: providers.JsonRpcProvider; 9 | 10 | private latestSnapshot: string; 11 | 12 | private constructor(provider: providers.JsonRpcProvider, initSnapshot: string) { 13 | this.provider = provider; 14 | this.latestSnapshot = initSnapshot; 15 | } 16 | 17 | static async create(provider: providers.JsonRpcProvider = hre.ethers.provider, log = false): Promise { 18 | const snapshot = await provider.send('evm_snapshot', []); 19 | if (log) console.log(`Captured EVM snapshot ${snapshot}`); 20 | return new EvmCheckpoint(provider, snapshot); 21 | } 22 | 23 | async revertTo(log = false) { 24 | const ok = await this.provider.send('evm_revert', [this.latestSnapshot]); 25 | if (!ok) { 26 | throw new Error(`Error reverting to EVM snapshot ${this.latestSnapshot}`); 27 | } 28 | if (log) console.log(`Reverted to EVM snapshot ${this.latestSnapshot}`); 29 | this.latestSnapshot = await this.provider.send('evm_snapshot', []); 30 | if (log) console.log(`Captured EVM snapshot ${this.latestSnapshot}`); 31 | } 32 | } 33 | 34 | export class EvmHelper { 35 | 36 | private readonly provider: providers.JsonRpcProvider; 37 | 38 | constructor(provider: providers.JsonRpcProvider) { 39 | this.provider = provider; 40 | } 41 | 42 | async setNextBlockTimestamp(datetime: string | number | BigNumber): Promise { 43 | let timestamp: number; 44 | if (typeof datetime === 'string') { 45 | timestamp = toTimestamp(datetime); 46 | } else { 47 | timestamp = BigNumber.from(datetime).toNumber(); 48 | } 49 | await this.provider.send('evm_setNextBlockTimestamp', [timestamp]); 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /app/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { decodeDoubleDiceCustomErrorData } from '@doubledice/platform/lib/contracts' 2 | import { BigNumber as BigDecimal } from 'bignumber.js' 3 | import { BigNumber as BigInteger } from 'ethers' 4 | 5 | export const flatten = (arrays: T[][]): T[] => Array.prototype.concat(...arrays) 6 | 7 | export const tryCatch = async (func: () => Promise): Promise => { 8 | try { 9 | await func() 10 | } catch (e: any) { 11 | if ( 12 | e.code && 13 | e.code === -32603 && 14 | e.data && 15 | e.data.message 16 | ) { 17 | if ( 18 | e.data.code && 19 | e.data.code === 3 && 20 | typeof e.data.message === 'string' && 21 | e.data.data && 22 | typeof e.data.data === 'string' && 23 | /^0x/.test(e.data.data) 24 | ) { 25 | const message = e.data.message as string 26 | const data = e.data.data as string 27 | const decoded = decodeDoubleDiceCustomErrorData(data) 28 | if (decoded) { 29 | alert(`${message}: ${decoded.name}(${decoded.formattedArgs})`) 30 | } else { 31 | alert(`${message}: ${data}`) 32 | } 33 | } else { 34 | alert(e.data.message) 35 | } 36 | } else { 37 | throw e 38 | } 39 | } 40 | } 41 | 42 | export const formatTimestamp = (timestamp: string | number): string => { 43 | return new Date(parseInt(timestamp.toString()) * 1000).toISOString().slice(0, 19).replace(/-/g, '\u2011') 44 | } 45 | 46 | export const sumBigDecimals = (values: BigDecimal[]): BigDecimal => { 47 | return values.reduce((a: BigDecimal, b: BigDecimal) => a.plus(b), new BigDecimal(0)) 48 | } 49 | 50 | export const sumNumbers = (values: number[]): number => { 51 | return values.reduce((a: number, b: number) => a + b, 0) 52 | } 53 | 54 | export const getSystemTimestamp = (): number => Math.floor(Date.now() / 1000) 55 | 56 | export const toFixedPointEthersBigNumber = (value: number, decimals: number): BigInteger => 57 | BigInteger.from(new BigDecimal(value).times(new BigDecimal(10).pow(decimals)).toString()) 58 | -------------------------------------------------------------------------------- /platform/contracts/library/ERC1155TokenIds.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; 6 | 7 | library ERC1155TokenIds { 8 | 9 | using SafeCastUpgradeable for uint256; 10 | 11 | /// @dev Any id having lower 5 bytes set to 0 is a valid virtual-floor id. 12 | /// A valid virtual-floor id doubles as both the virtual-floor's id, 13 | /// and as the ERC-1155 token id representing ownership of that virtual-floor. 14 | function isValidVirtualFloorId(uint256 value) internal pure returns (bool) { 15 | return value & 0xff_ff_ff_ff_ff == 0; 16 | } 17 | 18 | function extractVirtualFloorId(uint256 erc1155TokenId) internal pure returns (uint256) { 19 | return erc1155TokenId & ~uint256(0xff_ff_ff_ff_ff); 20 | } 21 | 22 | function destructure(uint256 erc1155TokenId) internal pure returns (uint256 vfId, uint8 outcomeIndex, uint32 timeslot) { 23 | vfId = erc1155TokenId & ~uint256(0xff_ff_ff_ff_ff); 24 | outcomeIndex = uint8((erc1155TokenId >> 32) & 0xff); 25 | timeslot = uint32(erc1155TokenId & 0xff_ff_ff_ff); 26 | } 27 | 28 | function vfOutcomeTimeslotIdOf( 29 | uint256 validVirtualFloorId, 30 | uint8 outcomeIndex, 31 | uint256 timeslot 32 | ) 33 | internal 34 | pure 35 | returns (uint256 tokenId) 36 | { 37 | // Since this function should always be called after the virtual-floor 38 | // has already been required to be in one of the non-None states, 39 | // and a virtual-floor can only be in a non-None state if it has a valid id, 40 | // then this assertion should never fail. 41 | assert(isValidVirtualFloorId(validVirtualFloorId)); 42 | 43 | tokenId = uint256(bytes32(abi.encodePacked( 44 | bytes27(bytes32(validVirtualFloorId)), // 27 bytes 45 | outcomeIndex, // + 1 byte 46 | timeslot.toUint32() // + 4 bytes 47 | ))); // = 32 bytes 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /platform/lib/contracts.ts: -------------------------------------------------------------------------------- 1 | export * from './generated/typechain-types'; 2 | export * from './generated/typechain-types/IDoubleDice'; 3 | export * from './helpers/sol-enums'; 4 | export type { 5 | VirtualFloorMetadataV1Struct, 6 | VirtualFloorMetadataV1Struct as RoomEventInfo, 7 | }; 8 | 9 | import { BytesLike, ethers } from 'ethers'; 10 | import { Result } from 'ethers/lib/utils'; 11 | import { DoubleDice__factory, GraphHelper__factory } from './generated/typechain-types'; 12 | import { VirtualFloorMetadataV1Struct } from './generated/typechain-types/GraphHelper'; 13 | import { EncodedVirtualFloorMetadataStruct } from './generated/typechain-types/IDoubleDice'; 14 | 15 | export const encodeVirtualFloorMetadata = (metadata: VirtualFloorMetadataV1Struct): EncodedVirtualFloorMetadataStruct => { 16 | const encodedWithSelector = GraphHelper__factory.createInterface().encodeFunctionData('encodeVirtualFloorMetadataV1', [metadata]); 17 | const encoded = ethers.utils.hexDataSlice(encodedWithSelector, 4); 18 | return { 19 | version: ethers.utils.hexZeroPad('0x01', 32), 20 | data: encoded 21 | }; 22 | }; 23 | 24 | type DecodedDoubleDiceCustomErrorData = { 25 | name: string; 26 | formattedArgs: string; 27 | } 28 | 29 | export const decodeDoubleDiceCustomErrorData = (encodedErrorData: BytesLike): DecodedDoubleDiceCustomErrorData | null => { 30 | const doubleDiceInterface = DoubleDice__factory.createInterface(); 31 | for (const errorFragment of Object.values(doubleDiceInterface.errors)) { 32 | let result: Result; 33 | try { 34 | result = doubleDiceInterface.decodeErrorResult(errorFragment, encodedErrorData); 35 | } catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any 36 | if ('reason' in e && /^data signature does not match error/.test(e.reason)) { 37 | continue; 38 | } else { 39 | return null; 40 | } 41 | } 42 | const formattedArgs = Object.entries(result) 43 | .filter(([name]) => !/^\d+$/.test(name)) // filter out numeric keys 44 | .map(([name, value]) => `${name}=${value}`) 45 | .join(','); 46 | return { 47 | name: errorFragment.name, 48 | formattedArgs, 49 | }; 50 | } 51 | return null; 52 | }; 53 | -------------------------------------------------------------------------------- /app/src/components/NewResultSourcesComponent.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 72 | 73 | 75 | -------------------------------------------------------------------------------- /platform/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers'; 2 | import '@nomiclabs/hardhat-waffle'; 3 | import '@typechain/hardhat'; 4 | import assert from 'assert'; 5 | import dotenv from 'dotenv'; 6 | import 'hardhat-abi-exporter'; 7 | import 'hardhat-gas-reporter'; 8 | import { HardhatUserConfig } from 'hardhat/types'; 9 | import 'solidity-coverage'; 10 | 11 | // // Commented out by default to avoid cyclic dependency (script relies on TypeChain, and TypeChain relies on this file) 12 | // import './scripts/decode-doubledice-custom-error'; 13 | 14 | 15 | const dotenvResult = dotenv.config(); 16 | if (dotenvResult.error) { 17 | throw dotenvResult.error; 18 | } 19 | 20 | const { 21 | PROVIDER_URL, 22 | OWNER_PRIVATE_KEY, 23 | } = process.env; 24 | 25 | assert(OWNER_PRIVATE_KEY); 26 | 27 | const config: HardhatUserConfig = { 28 | abiExporter: { 29 | path: './generated/abi', 30 | clear: true, 31 | flat: true, 32 | only: [ 33 | ':DoubleDice$', 34 | ':DummyUSDCoin$', 35 | ':GraphHelper$', 36 | ':IDoubleDice$', 37 | ':IDoubleDiceAdmin$', 38 | ':IERC20Metadata$', 39 | ], 40 | runOnCompile: true, 41 | }, 42 | networks: { 43 | hardhat: { 44 | chainId: 1337, 45 | }, 46 | ganache: { 47 | chainId: 1337, 48 | url: 'http://localhost:8545', 49 | }, 50 | rinkeby: { 51 | url: PROVIDER_URL, 52 | accounts: [OWNER_PRIVATE_KEY], 53 | chainId: 4, 54 | }, 55 | mumbai: { 56 | url: PROVIDER_URL, 57 | accounts: [OWNER_PRIVATE_KEY], 58 | chainId: 80001, 59 | }, 60 | }, 61 | solidity: { 62 | version: '0.8.12', 63 | settings: { 64 | optimizer: { 65 | enabled: true, 66 | runs: 100, 67 | }, 68 | }, 69 | }, 70 | typechain: { 71 | externalArtifacts: [ 72 | 'node_modules/@openzeppelin/contracts/build/contracts/ProxyAdmin.json', 73 | 'node_modules/@openzeppelin/contracts/build/contracts/TransparentUpgradeableProxy.json', 74 | ], 75 | outDir: 'lib/generated/typechain-types' 76 | }, 77 | gasReporter: { 78 | outputFile: 'gas-report.txt', 79 | noColors: true, 80 | excludeContracts: ['mock/'], 81 | }, 82 | }; 83 | 84 | export default config; -------------------------------------------------------------------------------- /platform/contracts/mock/FixedPointTypesMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../library/FixedPointTypes.sol"; 6 | 7 | contract FixedPointTypesMock { 8 | 9 | using FixedPointTypes for uint256; 10 | using FixedPointTypes for UFixed16x4; 11 | using FixedPointTypes for UFixed32x6; 12 | using FixedPointTypes for UFixed256x18; 13 | 14 | function add(UFixed256x18 a, UFixed256x18 b) external pure returns (UFixed256x18) { 15 | return a.add(b); 16 | } 17 | 18 | function sub(UFixed256x18 a, UFixed256x18 b) external pure returns (UFixed256x18) { 19 | return a.sub(b); 20 | } 21 | 22 | function mul0(UFixed256x18 a, uint256 b) external pure returns (UFixed256x18) { 23 | return a.mul0(b); 24 | } 25 | 26 | function div0(UFixed256x18 a, uint256 b) external pure returns (UFixed256x18) { 27 | return a.div0(b); 28 | } 29 | 30 | function divToUint256(UFixed256x18 a, UFixed256x18 b) external pure returns (uint256) { 31 | return a.divToUint256(b); 32 | } 33 | 34 | function floorToUint256(UFixed256x18 value) external pure returns (uint256) { 35 | return value.floorToUint256(); 36 | } 37 | 38 | function eq(UFixed256x18 a, UFixed256x18 b) external pure returns (bool) { 39 | return a.eq(b); 40 | } 41 | 42 | function gte(UFixed256x18 a, UFixed256x18 b) external pure returns (bool) { 43 | return a.gte(b); 44 | } 45 | 46 | function lte(UFixed256x18 a, UFixed256x18 b) external pure returns (bool) { 47 | return a.lte(b); 48 | } 49 | 50 | function toUFixed16x4(UFixed256x18 value) external pure returns (UFixed16x4 converted) { 51 | return value.toUFixed16x4(); 52 | } 53 | 54 | function toUFixed32x6(UFixed256x18 value) external pure returns (UFixed32x6 converted) { 55 | return value.toUFixed32x6(); 56 | } 57 | 58 | function toUFixed256x18__fromUint256(uint256 value) external pure returns (UFixed256x18) { 59 | return value.toUFixed256x18(); 60 | } 61 | 62 | function toUFixed256x18__fromUFixed16x4(UFixed16x4 value) external pure returns (UFixed256x18 converted) { 63 | return value.toUFixed256x18(); 64 | } 65 | 66 | function toUFixed256x18__fromUFixed32x6(UFixed32x6 value) external pure returns (UFixed256x18 converted) { 67 | return value.toUFixed256x18(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /platform/lib/room-event-info/common.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { JSONSchemaType } from 'ajv'; 2 | import addFormats from 'ajv-formats'; 3 | import { RoomEventInfo } from '../contracts'; 4 | 5 | const ajv = new Ajv(); 6 | addFormats(ajv); 7 | 8 | const schema: JSONSchemaType = { 9 | type: 'object', 10 | required: [ 11 | 'category', 12 | 'subcategory', 13 | 'title', 14 | 'description', 15 | 'isListed', 16 | 'opponents', 17 | 'outcomes', 18 | 'resultSources', 19 | 'discordChannelId', 20 | ], 21 | properties: { 22 | category: { type: 'string', minLength: 1 }, 23 | subcategory: { type: 'string', minLength: 1 }, 24 | title: { type: 'string', minLength: 1 }, 25 | description: { type: 'string', minLength: 1 }, 26 | isListed: { type: 'boolean' }, 27 | opponents: { 28 | type: 'array', 29 | items: { 30 | type: 'object', 31 | required: ['title', 'image'], 32 | properties: { 33 | title: { type: 'string', minLength: 1 }, 34 | image: { type: 'string', minLength: 1, format: 'uri' }, // for now a URL, later will be an IPFS content-uri 35 | }, 36 | additionalProperties: false, 37 | }, 38 | minItems: 1, 39 | uniqueItems: true, 40 | }, 41 | outcomes: { 42 | type: 'array', 43 | items: { 44 | type: 'object', 45 | required: ['title'], 46 | properties: { 47 | title: { type: 'string', minLength: 1 }, 48 | }, 49 | additionalProperties: false, 50 | }, 51 | minItems: 2, 52 | maxItems: 256, 53 | uniqueItems: true, 54 | }, 55 | resultSources: { 56 | type: 'array', 57 | items: { 58 | type: 'object', 59 | required: ['title', 'url'], 60 | properties: { 61 | title: { type: 'string', minLength: 1 }, 62 | url: { type: 'string', minLength: 1, format: 'uri' }, 63 | }, 64 | additionalProperties: false, 65 | }, 66 | minItems: 1, 67 | uniqueItems: true, 68 | }, 69 | discordChannelId: { 70 | type: 'string', 71 | minLength: 1, 72 | }, 73 | extraData: { 74 | type: 'string', 75 | pattern: '^0x([0-9a-fA-F]{2})*$', 76 | }, 77 | }, 78 | additionalProperties: false, 79 | }; 80 | 81 | // ToDo: Extend function so that in addition to JSON validation, 82 | // it also checks that outcome index values are correct and in order, 83 | // category and subcategory match constraints, etc. 84 | export const validateRoomEventInfo = ajv.compile(schema); 85 | -------------------------------------------------------------------------------- /app/src/mm.ts: -------------------------------------------------------------------------------- 1 | export type HexPrefixed = `0x${string}`; 2 | 3 | interface RequestArguments { 4 | method: string; 5 | // params?: unknown[] | Record; 6 | params?: [...T] 7 | } 8 | 9 | export interface EthereumProvider { 10 | 11 | isMetaMask?: boolean; 12 | 13 | request(args: RequestArguments): Promise; 14 | 15 | on(eventName: 'accountsChanged', listener: (accounts: [HexPrefixed]) => void): void; 16 | 17 | on(eventName: 'chainChanged', listener: (chainId: number) => void): void; 18 | } 19 | 20 | interface RequestedPermissions { 21 | [methodName: string]: Record 22 | } 23 | 24 | interface Web3WalletPermission { 25 | // The name of the method corresponding to the permission 26 | parentCapability: string; 27 | 28 | // The date the permission was granted, in UNIX epoch time 29 | date?: number; 30 | } 31 | 32 | export class EthereumProviderHelper { 33 | private ethereum: EthereumProvider 34 | 35 | constructor(ethereum: EthereumProvider) { 36 | this.ethereum = ethereum 37 | this.ethereum.on('accountsChanged', (accounts) => this.onAccountsChanged(accounts)) 38 | this.ethereum.on('chainChanged', (chainId) => this.onChainChanged(chainId)) 39 | } 40 | 41 | onAccountsChanged(accounts: [HexPrefixed]): void { 42 | alert(`accounts => ${accounts}`) 43 | location.reload() 44 | } 45 | 46 | onChainChanged(chainId: number): void { 47 | alert(`chainId => ${chainId}`) 48 | location.reload() 49 | } 50 | 51 | async init(): Promise { 52 | const permissions = await this.walletGetPermissions() 53 | if (!permissions.find(({ parentCapability }) => parentCapability === 'eth_accounts')) { 54 | const permissions = await this.walletRequestPermissions({ eth_accounts: {} }) 55 | if (permissions.find(({ parentCapability }) => parentCapability === 'eth_accounts')) { 56 | console.log('eth_accounts permission successfully requested!') 57 | } 58 | } 59 | } 60 | 61 | async walletGetPermissions(): Promise { 62 | const permissionsArray = await this.ethereum.request<[], Web3WalletPermission[]>({ 63 | method: 'wallet_getPermissions' 64 | }) 65 | return permissionsArray 66 | } 67 | 68 | async walletRequestPermissions(requestedPermissions: RequestedPermissions): Promise { 69 | const permissions = await this.ethereum.request<[RequestedPermissions], Web3WalletPermission[]>({ 70 | method: 'wallet_requestPermissions', 71 | params: [requestedPermissions] 72 | }) 73 | return permissions 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/components/NewOpponentsComponent.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 81 | 82 | 84 | -------------------------------------------------------------------------------- /platform/subgraph/subgraph.template.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.2 2 | description: DoubleDice platform 3 | schema: 4 | file: ../schema.graphql 5 | dataSources: 6 | - kind: ethereum/contract 7 | name: DoubleDice 8 | network: "${DOUBLEDICE_CONTRACT_NETWORK}" 9 | source: 10 | address: "${DOUBLEDICE_CONTRACT_ADDRESS}" 11 | abi: DoubleDice 12 | startBlock: ${DOUBLEDICE_CONTRACT_START_BLOCK} 13 | mapping: 14 | kind: ethereum/events 15 | apiVersion: 0.0.6 16 | language: wasm/assemblyscript 17 | entities: 18 | - Category 19 | - Opponent 20 | - Outcome 21 | - OutcomeTimeslot 22 | - OutcomeTimeslotTransfer 23 | - PaymentToken 24 | - ResultSource 25 | - Subcategory 26 | - User 27 | - UserOutcome 28 | - UserOutcomeTimeslot 29 | - VirtualFloor 30 | - VirtualFloorsAggregate 31 | abis: 32 | - name: DoubleDice 33 | file: ./DoubleDice.no-custom-errors.json 34 | - name: GraphHelper 35 | file: ../../generated/abi/GraphHelper.json 36 | - name: IERC20Metadata 37 | file: ../../generated/abi/IERC20Metadata.json 38 | eventHandlers: 39 | - event: PaymentTokenWhitelistUpdate(indexed address,bool) 40 | handler: handlePaymentTokenWhitelistUpdate 41 | - event: VirtualFloorCreation(indexed uint256,indexed address,uint256,uint256,uint256,uint32,uint32,uint32,uint8,address,uint256,uint256,uint256,(bytes32,bytes)) 42 | handler: handleVirtualFloorCreation 43 | - event: UserCommitment(indexed uint256,indexed address,uint8,uint256,uint256,uint256,uint256) 44 | handler: handleUserCommitment 45 | - event: TransferSingle(indexed address,indexed address,indexed address,uint256,uint256) 46 | handler: handleTransferSingle 47 | - event: TransferBatch(indexed address,indexed address,indexed address,uint256[],uint256[]) 48 | handler: handleTransferBatch 49 | - event: VirtualFloorCancellationUnresolvable(indexed uint256) 50 | handler: handleVirtualFloorCancellationUnresolvable 51 | - event: VirtualFloorCancellationFlagged(indexed uint256,string) 52 | handler: handleVirtualFloorCancellationFlagged 53 | - event: VirtualFloorResolution(indexed uint256,uint8,uint8,uint256,uint256,uint256) 54 | handler: handleVirtualFloorResolution 55 | - event: CreationQuotaAdjustments((address,int256)[]) 56 | handler: handleCreationQuotaAdjustments 57 | - event: ResultUpdate(indexed uint256,address,uint8,uint8) 58 | handler: handleResultUpdate 59 | file: ../assemblyscript/mapping.ts 60 | -------------------------------------------------------------------------------- /platform/lib/room-event-info/common.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiSubset from 'chai-subset'; 3 | import { RoomEventInfo, validateRoomEventInfo } from './common'; 4 | 5 | chai.use(chaiSubset); 6 | 7 | const valid: Readonly = { 8 | category: 'sports', 9 | subcategory: 'football', 10 | title: 'Finland vs. Argentina', 11 | description: 'Finland vs. Argentina FIFA 2022 world cup final', 12 | isListed: true, 13 | opponents: [ 14 | { 15 | title: 'Finland', 16 | image: 'https://upload.wikimedia.org/wikipedia/commons/3/31/Huuhkajat_logo.svg', 17 | }, 18 | { 19 | title: 'Argentina', 20 | image: 'https://upload.wikimedia.org/wikipedia/en/c/c1/Argentina_national_football_team_logo.svg', 21 | }, 22 | ], 23 | outcomes: [ 24 | { index: 0, title: 'Finland win' }, 25 | { index: 1, title: 'Argentina win' }, 26 | { index: 2, title: 'Tie' }, 27 | ], 28 | resultSources: [ 29 | { title: 'Official FIFA result page', url: 'https://www.youtube.com/watch?v=BEt3DcEQUbs' }, 30 | ], 31 | }; 32 | 33 | describe('validateEventInfo', () => { 34 | 35 | it('valid data', () => { 36 | expect(validateRoomEventInfo(valid)).to.be.true; 37 | }); 38 | 39 | describe('invalid data', () => { 40 | 41 | it('missing category', () => { 42 | const data: Partial = { ...valid }; 43 | expect(validateRoomEventInfo(data)).to.be.true; 44 | delete data.category; 45 | expect(validateRoomEventInfo(data)).to.be.false; 46 | }); 47 | 48 | it('missing isListed', () => { 49 | const data: Partial = { ...valid }; 50 | expect(validateRoomEventInfo(data)).to.be.true; 51 | delete data.isListed; 52 | expect(validateRoomEventInfo(data)).to.be.false; 53 | }); 54 | 55 | it('empty title', () => { 56 | expect(validateRoomEventInfo({ ...valid, title: 'abc' })).to.be.true; 57 | expect(validateRoomEventInfo({ ...valid, title: 'ab' })).to.be.true; 58 | expect(validateRoomEventInfo({ ...valid, title: 'a' })).to.be.true; 59 | expect(validateRoomEventInfo({ ...valid, title: '' })).to.be.false; 60 | }); 61 | 62 | it('just 1 opponent', () => { 63 | const { opponents: [oppenent0, opponent1] } = valid; 64 | expect(validateRoomEventInfo({ ...valid, opponents: [oppenent0, opponent1] })).to.be.true; 65 | expect(validateRoomEventInfo({ ...valid, opponents: [oppenent0] })).to.be.false; 66 | }); 67 | 68 | it('just 1 outcome', () => { 69 | const { outcomes: [outcome0, outcome1] } = valid; 70 | expect(validateRoomEventInfo({ ...valid, outcomes: [outcome0, outcome1] })).to.be.true; 71 | expect(validateRoomEventInfo({ ...valid, outcomes: [outcome0] })).to.be.false; 72 | }); 73 | 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /platform/contracts/library/VirtualFloorCreationParamsUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "../BaseDoubleDice.sol"; 6 | import "../interface/IDoubleDice.sol"; 7 | import "./ERC1155TokenIds.sol"; 8 | import "./FixedPointTypes.sol"; 9 | 10 | // An estimate of how much the block.timestamp could possibly deviate 11 | // from the real timestamp, and still be accepted by the network. 12 | uint256 constant _MAX_POSSIBLE_BLOCK_TIMESTAMP_DISCREPANCY = 60 seconds; 13 | 14 | // CR-01: If a buffer between tClose and tResolve were not to be enforced, 15 | // it would then be possible to create a VF with tClose == tResolve, 16 | // and a malicious miner could perform the following attack: 17 | // At tUniversal = tResolve, 18 | // when the winning outcome of the VF becomes known to the public, 19 | // the miner would manipulate block.timestamp by a few seconds 20 | // e.g. the miner would set block.timestamp = tUniversal - a few seconds, 21 | // and the network would accept this block. 22 | // But despite the VF result being known in the outside world, 23 | // from the contract's point of view, it is still block.timestamp <= tClose, 24 | // and the miner would take advantage of this to commit an amount of money 25 | // to the outcome that in the outside world is known to be the winner. 26 | // 27 | // By enforcing a buffer between tClose and tResolve, 28 | // i.e. by forcing (tResolve - tClose) to be considerably larger than the largest amount of time 29 | // by which block.timestamp could possibly be manipulated, such an attack is averted. 30 | uint256 constant _MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE = 10 * _MAX_POSSIBLE_BLOCK_TIMESTAMP_DISCREPANCY; 31 | 32 | library VirtualFloorCreationParamsUtils { 33 | 34 | using ERC1155TokenIds for uint256; 35 | using FixedPointTypes for UFixed256x18; 36 | 37 | function validatePure(VirtualFloorCreationParams calldata $) internal pure { 38 | { 39 | if (!$.virtualFloorId.isValidVirtualFloorId()) revert InvalidVirtualFloorId(); 40 | } 41 | { 42 | if (!($.betaOpen_e18.gte(_BETA_CLOSE))) revert BetaOpenTooSmall(); 43 | } 44 | { 45 | if (!($.creationFeeRate_e18.lte(UFIXED256X18_ONE))) revert CreationFeeRateTooLarge(); 46 | } 47 | { 48 | if (!($.tOpen < $.tClose && $.tClose + _MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE <= $.tResolve)) revert InvalidTimeline(); 49 | } 50 | { 51 | if (!($.nOutcomes >= 2)) revert NotEnoughOutcomes(); 52 | } 53 | } 54 | 55 | // Allow creation to happen up to 10% into the Open period, 56 | // to be a bit tolerant to mining delays. 57 | function tCreateMax(VirtualFloorCreationParams calldata params) internal pure returns (uint256) { 58 | return params.tOpen + (params.tClose - params.tOpen) / 10; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/netlify/functions/erc1155-token-metadata/index.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@netlify/functions'; 2 | import { request, gql } from 'graphql-request'; 3 | 4 | const handler: Handler = async (event, context) => { 5 | 6 | // Ex. If the URl is http://localhost:8888/api/metadata/000000000000000000000000000000000000000000000000000000000004cce0.json 7 | // The [event.path] is api/metadata/000000000000000000000000000000000000000000000000000000000004cce0.json 8 | // The hexString is 000000000000000000000000000000000000000000000000000000000004cce0 9 | 10 | const splitPath = event.path.split('/'); 11 | const hexString = splitPath[splitPath.length - 1].split('.')[0]; 12 | 13 | if (!validateHexDecimal(hexString)) { 14 | return { statusCode: 400 }; 15 | } 16 | const queryResult = await request('http://127.0.0.1:8000/subgraphs/name/doubledice-com/doubledice-platform', getQuery(`0x${hexString}`)); 17 | 18 | if (!queryResult.data.virtualFloorOutcomeTimeslot) { 19 | return { statusCode: 404 }; 20 | } 21 | 22 | const result = queryResult.data.virtualFloorOutcomeTimeslot; 23 | return { 24 | statusCode: 200, 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | body: JSON.stringify({ 29 | name: `${result.outcome.virtualFloor.id}#${result.outcome.index}/${result.timeslot.minTimestamp}`, 30 | description: `Description for VirtualFloor id ${result.outcome.virtualFloor.id}; Outcome #${result.outcome.index}; Timeslot ${result.timeslot.minTimestamp}`, 31 | image: `data:image/svg+xml;charset=UTF-8,%3csvg width='800' height='600' xmlns='http://www.w3.org/2000/svg'%3e%3cg%3e%3ctitle%3eLayer 1%3c/title%3e%3crect stroke='%23000' id='svg_1' height='124' width='736.99997' y='151' x='30' fill='%23fff'/%3e%3ctext xml:space='preserve' text-anchor='start' font-family='Noto Sans JP' font-size='24' stroke-width='0' id='svg_2' y='218' x='67' stroke='%23000' fill='%23000000'%3e VirtualFloor id ${result.outcome.virtualFloor.id}; Outcome %${result.outcome.index}; Timeslot ${result.timeslot.minTimestamp} %3c/text%3e%3c/g%3e%3c/svg%3e`, 32 | decimals: result.outcome.virtualFloor.paymentToken.decimals, 33 | }), 34 | }; 35 | 36 | }; 37 | 38 | const getQuery = (erc1155TokenId: string): string => { 39 | 40 | const query = gql` 41 | { 42 | virtualFloorOutcomeTimeslot(where: { id: ${erc1155TokenId} }){ 43 | timeslot { 44 | minTimestamp 45 | beta 46 | } 47 | outcome { 48 | index 49 | virtualFloor { 50 | id 51 | paymentToken { 52 | name 53 | symbol 54 | decimals 55 | } 56 | state 57 | } 58 | } 59 | } 60 | 61 | } 62 | `; 63 | return query; 64 | }; 65 | 66 | const validateHexDecimal = (hexString: string): boolean => { 67 | return hexString.length == 64 && /^[0-9a-f]+$/.test(hexString); 68 | }; 69 | 70 | export { handler }; 71 | 72 | -------------------------------------------------------------------------------- /platform/docs/STATE.md: -------------------------------------------------------------------------------- 1 | # Virtual-floor state diagrams 2 | 3 | ## ChallengeableCreatorOracle 4 | 5 | We now explode the `RunningOrClosed_ClosedResolvable` state into further sub-states, as stored on the `ChallengeableCreatorOracle` contract: 6 | 7 | ```mermaid 8 | stateDiagram-v2 9 | %% Conditional states 10 | state RunningOrClosed_Closed <> 11 | state RunningOrClosed_ClosedResolvable_ResultComplete <> 12 | state resolutionType <> 13 | 14 | [*] --> None 15 | 16 | None --> RunningOrClosed_Running: createVirtualFloor() 17 | 18 | RunningOrClosed_* --> CancelledFlagged: cancelFlagged() 19 | 20 | RunningOrClosed_Running --> RunningOrClosed_Closed: t ≥ tClose 21 | 22 | RunningOrClosed_Closed --> RunningOrClosed_ClosedUnresolvable: has commits to < 2 outcomes 23 | RunningOrClosed_Closed --> RunningOrClosed_ClosedPreResolvable: has commits to ≥ 2 outcomes 24 | 25 | RunningOrClosed_ClosedUnresolvable --> CancelledUnresolvable: cancelUnresolvable() 26 | 27 | RunningOrClosed_ClosedPreResolvable --> RunningOrClosed_ClosedResolvable_ResultNone: t ≥ tResolve 28 | 29 | %% RunningOrClosed_ClosedResolvable_* --> RunningOrClosed_ClosedResolvable_ResultComplete 30 | RunningOrClosed_ClosedResolvable_ResultNone --> RunningOrClosed_ClosedResolvable_ResultSet: setResult()\n@ t ≤ tResultSetMax 31 | RunningOrClosed_ClosedResolvable_ResultSet --> RunningOrClosed_ClosedResolvable_ResultChallenged: challengeSetResult()\n@ t ≤ tResultChallengeMax 32 | RunningOrClosed_ClosedResolvable_ResultChallenged --> RunningOrClosed_ClosedResolvable_ResultComplete: finalizeChallenge() 33 | RunningOrClosed_ClosedResolvable_ResultSet --> RunningOrClosed_ClosedResolvable_ResultComplete: confirmUnchallengedResult()\n@ t > tResultChallengeMax 34 | RunningOrClosed_ClosedResolvable_ResultNone --> RunningOrClosed_ClosedResolvable_ResultComplete: finalizeUnsetResult()\n@ t > tResultSetMax 35 | 36 | RunningOrClosed_ClosedResolvable_ResultComplete --> resolutionType: _resolve() 37 | resolutionType --> CancelledResolvedNoWinners: CancelledNoWinners 38 | resolutionType --> ResolvedWinners: Winners 39 | 40 | %% Stop-states 41 | CancelledFlagged --> [*] 42 | CancelledUnresolvable --> [*] 43 | CancelledResolvedNoWinners --> [*] 44 | ResolvedWinners --> [*] 45 | ``` 46 | 47 | The only details that are not visible in this diagram are that: 48 | 1. When the base contract’s “computed” state (as reported by `getVirtualFloorState()`) goes into `CancelledResolvedNoWinners | ResolvedWinners`, in the extending `ChallengeableCreatorOracle` contract the corresponding `Resolution.state` for that VF will be moved (in parallel) to state `ResolutionState.Complete`. 49 | 2. If a VF set-result has been challenged, and therefore its `Resolution.state` in `ChallengeableCreatorOracle` is `ResolutionState.Challenged`, if at that moment the base contract’s state is forced by the platform-admin into `CancelledFlagged`, the `Resolution.state` will be moved (in parallel) into `ResolutionState.ChallengeCancelled`. 50 | -------------------------------------------------------------------------------- /platform/scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { BigNumber } from 'ethers'; 3 | import { ethers } from 'hardhat'; 4 | import { 5 | deployDoubleDice, 6 | deployDummyUSDCoin, 7 | deployDummyWrappedBTC, 8 | deployGraphHelper, 9 | toFp18 10 | } from '../helpers'; 11 | import { 12 | DummyUSDCoin, 13 | DummyUSDCoin__factory, 14 | DummyWrappedBTC, 15 | DummyWrappedBTC__factory 16 | } from '../lib/contracts'; 17 | 18 | const { 19 | CHAIN_ID, 20 | OWNER_ADDRESS, 21 | INIT_TOKEN_METADATA_URI_TEMPLATE, 22 | INIT_CONTRACT_URI, 23 | INIT_PLATFORM_FEE_RATE, 24 | INIT_PLATFORM_FEE_BENEFICIARY, 25 | DEPLOYED_USDC_ADDRESS = '', 26 | DEPLOYED_WBTC_ADDRESS = '', 27 | SKIP_DEPLOY_GRAPH_HELPER = '' 28 | } = process.env; 29 | 30 | async function main() { 31 | 32 | assert(CHAIN_ID); 33 | assert(OWNER_ADDRESS); 34 | assert(INIT_TOKEN_METADATA_URI_TEMPLATE); 35 | assert(INIT_PLATFORM_FEE_RATE); 36 | assert(INIT_PLATFORM_FEE_BENEFICIARY); 37 | assert(INIT_CONTRACT_URI); 38 | 39 | const { chainId } = await ethers.provider.getNetwork(); 40 | assert(parseInt(CHAIN_ID) === chainId, `${CHAIN_ID} !== ${chainId}; wrong .env config?`); 41 | 42 | const deployer = await ethers.getSigner(OWNER_ADDRESS); 43 | 44 | let tokenUSDC: DummyUSDCoin; 45 | if (DEPLOYED_USDC_ADDRESS) { 46 | tokenUSDC = DummyUSDCoin__factory.connect(DEPLOYED_USDC_ADDRESS, deployer); 47 | } else { 48 | tokenUSDC = await deployDummyUSDCoin(deployer); 49 | } 50 | 51 | let tokenWBTC: DummyWrappedBTC; 52 | if (DEPLOYED_WBTC_ADDRESS) { 53 | tokenWBTC = DummyWrappedBTC__factory.connect(DEPLOYED_WBTC_ADDRESS, deployer); 54 | } else { 55 | tokenWBTC = await deployDummyWrappedBTC(deployer); 56 | } 57 | 58 | const contract = await deployDoubleDice({ 59 | deployer: deployer, 60 | deployArgs: [], 61 | initializeArgs: [ 62 | { 63 | tokenMetadataUriTemplate: INIT_TOKEN_METADATA_URI_TEMPLATE, 64 | platformFeeRate_e18: toFp18(INIT_PLATFORM_FEE_RATE), 65 | platformFeeBeneficiary: INIT_PLATFORM_FEE_BENEFICIARY, 66 | contractURI: INIT_CONTRACT_URI, 67 | }, 68 | tokenUSDC.address, 69 | ] 70 | }); 71 | 72 | console.log(`Whitelisting USDC@${tokenUSDC.address} on DoubleDice contract`); 73 | await ((await contract.updatePaymentTokenWhitelist(tokenUSDC.address, true)).wait()); 74 | 75 | console.log(`Whitelisting WBTC@${tokenWBTC.address} on DoubleDice contract`); 76 | await ((await contract.updatePaymentTokenWhitelist(tokenWBTC.address, true)).wait()); 77 | 78 | // Read ProxyAdmin address off the DD contract 79 | // See https://eips.ethereum.org/EIPS/eip-1967#admin-address 80 | const ADMIN_SLOT = ethers.utils.hexZeroPad(BigNumber.from(ethers.utils.keccak256(ethers.utils.toUtf8Bytes('eip1967.proxy.admin'))).sub(1).toHexString(), 32); 81 | const storedAdminSlotValue = await ethers.provider.getStorageAt(contract.address, ADMIN_SLOT); 82 | const fixedStoredAdminSlotValue = ethers.utils.hexZeroPad(storedAdminSlotValue, 32); // should be a bytes32, but sometimes it isn't, so we fix it 83 | const proxyAdminAddress = ethers.utils.hexDataSlice(fixedStoredAdminSlotValue, 12); 84 | 85 | if (!(/^(true|yes|1)$/i.test(SKIP_DEPLOY_GRAPH_HELPER))) { 86 | await deployGraphHelper({ deployer, proxyAdminAddress }); 87 | } 88 | } 89 | 90 | main() 91 | .then(() => process.exit(0)) 92 | .catch(error => { 93 | console.error(error); 94 | process.exit(1); 95 | }); 96 | -------------------------------------------------------------------------------- /platform/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | | Commit | Commit message | Comment | 2 | | ---------- | ----------------------------------------------------------------------- | ------- | 3 | | `5bd440da` | Delete TIMESLOT_DURATION optimization in return for less complexity | Commit on which initial audit report is based | 4 | | `e0fca9c9` | audit/ME-01: Replace remaining ERC-20 .transfer with .safeTransfer | Direct audit recommendation | 5 | | `10abf7ca` | audit/ME-02: Make commitToVirtualFloor nonReentrant | Direct audit recommendation | 6 | | `6def4aa8` | audit/ME-02: Comment on all implicitly nonReentrant external calls | Comments | 7 | | `de79f3d4` | audit/MI-01: Restrict floating ^0.8.0 pragma to 0.8.12 | Direct audit recommendation | 8 | | `824a38c8` | Configure solhint, handle all warnings except `not-rely-on-time` | Comments | 9 | | `b5a38f2d` | audit/CR-01: Comment on all block.timestamp manipulation risks | Review of all block.timestamp uses, as pointed out by audit | 10 | | `09c5c068` | audit/CR-01: Enforce considerable delay between tClose and tResolve | Audit recommendation enforced as a `require` | 11 | | `41327e96` | audit/CR-01: Make commitToVirtualFloor accept (optional) deadline param | Tackled another potential timestamp-related issue | 12 | | `6f40b340` | Add contractURI method for OpenSea storefront-level metadata | Minor: An extra parameter on the contract, decoupled from the rest of the contract | 13 | | `e2ffe552` | Add metadata extraData field | Minor: An extra field on the `VirtualFloorMetadataV1` struct | 14 | | `b785fce1` | contract: Minor reordering of variables, for consistency | | 15 | | `408e2bfb` | Reconfigure optimizer.runs from 200 => 100 for smaller contract | Quick way to stay below contract-size ceiling without having to change code.| 16 | | `cda8cae1` | New OPERATOR_ROLE, responsible for day-to-day operations | Still using OpenZeppelin’s AccessControl library, simply reassigned some functions to a new role.| 17 | | `913d66c6`

`cec3f987` | contract: Move SimpleOracle into examples/

Refactor graph-codegen as script, reorganize subgraph project| *A series of commits that do not impact the deployed contract bytecode in any way* | 18 | | `03b3de76` | Revert "Work around graphprotocol ethereum.decode limitation" | Minor: Changes to `VirtualFloorMetadataV1` struct| 19 | | `aae68fcc` | Finally work around Graph ethereum.decode tuple-array bug cleanly | New `GraphHelper` contract, but is outside audit scope.
Audited `DoubleDice` contract does not reference it;
its only purpose is for the Graph indexer to invoke a pure function on it 20 | | `3fea8d3a` | Fix: Refund bonusAmount in remaining 2- VF cancellation scenarios | Contract bugfix | 21 | | `f7363db4` | audit/EN-01: Drop redundant call to (empty) super._beforeTokenTransfer | | 22 | | `c32436c9` | audit/EN-01: Check paused() before anything else | Direct audit minor enhancment recommendation | 23 | | `8e857418` | audit/EN-01: Note about order of checks | Comments | 24 | | `1b91236f` | audit/EN-02: Assert if-condition handles all possible VirtualFloorInternalState values | Direct audit enhancement recommendation | 25 | | `18b318cd` | Make use of state local variable | Minor | 26 | -------------------------------------------------------------------------------- /platform/test/sol-enums.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Signer } from 'ethers'; 3 | import { ethers } from 'hardhat'; 4 | import { 5 | ResolutionState, 6 | ResolutionStateWrapper__factory, 7 | ResultUpdateAction, 8 | ResultUpdateActionWrapper__factory, 9 | VirtualFloorResolutionType, 10 | VirtualFloorResolutionTypeWrapper__factory, 11 | VirtualFloorState, 12 | VirtualFloorStateWrapper__factory 13 | } from '../lib/contracts'; 14 | 15 | describe('Check manual Solidity-enum-type TypeScript analogs against actual values', () => { 16 | 17 | let signer: Signer; 18 | 19 | before(async () => { 20 | [signer] = await ethers.getSigners(); 21 | }); 22 | 23 | it('VirtualFloorState', async () => { 24 | const lib = await new VirtualFloorStateWrapper__factory(signer).deploy(); 25 | await lib.deployed(); 26 | 27 | expect(VirtualFloorState.None).to.eq(await lib.None()); 28 | expect(VirtualFloorState.Active_Open_MaybeResolvableNever).to.eq(await lib.Active_Open_MaybeResolvableNever()); 29 | expect(VirtualFloorState.Active_Open_ResolvableLater).to.eq(await lib.Active_Open_ResolvableLater()); 30 | expect(VirtualFloorState.Active_Closed_ResolvableNever).to.eq(await lib.Active_Closed_ResolvableNever()); 31 | expect(VirtualFloorState.Active_Closed_ResolvableLater).to.eq(await lib.Active_Closed_ResolvableLater()); 32 | expect(VirtualFloorState.Active_Closed_ResolvableNow).to.eq(await lib.Active_Closed_ResolvableNow()); 33 | expect(VirtualFloorState.Claimable_Payouts).to.eq(await lib.Claimable_Payouts()); 34 | expect(VirtualFloorState.Claimable_Refunds_ResolvedNoWinners).to.eq(await lib.Claimable_Refunds_ResolvedNoWinners()); 35 | expect(VirtualFloorState.Claimable_Refunds_ResolvableNever).to.eq(await lib.Claimable_Refunds_ResolvableNever()); 36 | expect(VirtualFloorState.Claimable_Refunds_Flagged).to.eq(await lib.Claimable_Refunds_Flagged()); 37 | }); 38 | 39 | it('VirtualFloorResolutionType', async () => { 40 | const lib = await new VirtualFloorResolutionTypeWrapper__factory(signer).deploy(); 41 | await lib.deployed(); 42 | expect(VirtualFloorResolutionType.NoWinners).to.eq(await lib.NoWinners()); 43 | expect(VirtualFloorResolutionType.Winners).to.eq(await lib.Winners()); 44 | }); 45 | 46 | it('ResultUpdateAction', async () => { 47 | const lib = await new ResultUpdateActionWrapper__factory(signer).deploy(); 48 | await lib.deployed(); 49 | expect(ResultUpdateAction.AdminFinalizedUnsetResult).to.eq(await lib.AdminFinalizedUnsetResult()); 50 | expect(ResultUpdateAction.CreatorSetResult).to.eq(await lib.CreatorSetResult()); 51 | expect(ResultUpdateAction.SomeoneConfirmedUnchallengedResult).to.eq(await lib.SomeoneConfirmedUnchallengedResult()); 52 | expect(ResultUpdateAction.SomeoneChallengedSetResult).to.eq(await lib.SomeoneChallengedSetResult()); 53 | expect(ResultUpdateAction.AdminFinalizedChallenge).to.eq(await lib.AdminFinalizedChallenge()); 54 | }); 55 | 56 | it('ResolutionState', async () => { 57 | const lib = await new ResolutionStateWrapper__factory(signer).deploy(); 58 | await lib.deployed(); 59 | expect(ResolutionState.None).to.eq(await lib.None()); 60 | expect(ResolutionState.Set).to.eq(await lib.Set()); 61 | expect(ResolutionState.Challenged).to.eq(await lib.Challenged()); 62 | expect(ResolutionState.ChallengeCancelled).to.eq(await lib.ChallengeCancelled()); 63 | expect(ResolutionState.Complete).to.eq(await lib.Complete()); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /app/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 22 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/assets/doubledice-usdc-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 22 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /platform/contracts/VirtualFloorMetadataValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "./BaseDoubleDice.sol"; 6 | import "./library/Utils.sol"; 7 | 8 | struct VirtualFloorMetadataOpponent { 9 | string title; 10 | string image; 11 | } 12 | 13 | struct VirtualFloorMetadataOutcome { 14 | string title; 15 | } 16 | 17 | struct VirtualFloorMetadataResultSource { 18 | string title; 19 | string url; 20 | } 21 | 22 | struct VirtualFloorMetadataV1 { 23 | string category; 24 | string subcategory; 25 | string title; 26 | string description; 27 | bool isListed; 28 | VirtualFloorMetadataOpponent[] opponents; 29 | VirtualFloorMetadataOutcome[] outcomes; 30 | VirtualFloorMetadataResultSource[] resultSources; 31 | string discordChannelId; 32 | bytes extraData; 33 | } 34 | 35 | 36 | error InvalidMetadataVersion(); 37 | 38 | error MetadataOpponentArrayLengthMismatch(); 39 | 40 | error ResultSourcesArrayLengthMismatch(); 41 | 42 | error InvalidOutcomesArrayLength(); 43 | 44 | error TooFewOpponents(); 45 | 46 | error TooFewResultSources(); 47 | 48 | error EmptyCategory(); 49 | 50 | error EmptySubcategory(); 51 | 52 | error EmptyTitle(); 53 | 54 | error EmptyDescription(); 55 | 56 | error EmptyDiscordChannelId(); 57 | 58 | 59 | contract VirtualFloorMetadataValidator is BaseDoubleDice { 60 | 61 | using Utils for string; 62 | 63 | function __VirtualFloorMetadataValidator_init(BaseDoubleDiceInitParams calldata params) internal onlyInitializing { 64 | __BaseDoubleDice_init(params); 65 | } 66 | 67 | function _onVirtualFloorCreation(VirtualFloorCreationParams calldata params) internal virtual override { 68 | uint256 version = uint256(params.metadata.version); 69 | if (!(version == 1)) revert InvalidMetadataVersion(); 70 | 71 | (VirtualFloorMetadataV1 memory metadata) = abi.decode(params.metadata.data, (VirtualFloorMetadataV1)); 72 | 73 | // `nOutcomes` could simply be taken to be `metadata.outcomes.length` and this `require` could then be dropped. 74 | // But for now we choose to make a clear distinction between "essential" data (that needs to be stored on-chain) 75 | // and "non-essential" data (data that we want to commit to and that is required in the frontend, 76 | // but that is is not essential for the operation of the smart-contract). 77 | // To this end, we group all non-essential data in the `metadata` parameter, 78 | // we require a separate `nOutcomes` "essential" argument to be passed, 79 | // and we enforce consistency with this check. 80 | if (!(metadata.outcomes.length == params.nOutcomes)) revert InvalidOutcomesArrayLength(); 81 | 82 | if (!(metadata.opponents.length >= 1)) revert TooFewOpponents(); 83 | 84 | if (!(metadata.resultSources.length >= 1)) revert TooFewResultSources(); 85 | 86 | if (!(!metadata.category.isEmpty())) revert EmptyCategory(); 87 | 88 | if (!(!metadata.subcategory.isEmpty())) revert EmptySubcategory(); 89 | 90 | if (!(!metadata.title.isEmpty())) revert EmptyTitle(); 91 | 92 | if (!(!metadata.description.isEmpty())) revert EmptyDescription(); 93 | 94 | if (!(!metadata.discordChannelId.isEmpty())) revert EmptyDiscordChannelId(); 95 | } 96 | 97 | /// @dev See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 98 | uint256[50] private __gap; 99 | } 100 | -------------------------------------------------------------------------------- /platform/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { BigNumber, BigNumberish, ContractReceipt, ethers } from 'ethers'; 3 | import { 4 | encodeVirtualFloorMetadata, 5 | RoomEventInfo 6 | } from '../lib/contracts'; 7 | 8 | export const toFp18 = (value: number | string): BigNumber => { 9 | const numericValue = typeof value === 'number' ? value : parseFloat(value); 10 | const sign = Math.sign(numericValue); 11 | const magnitude = Math.abs(numericValue); 12 | if (magnitude === 0) { 13 | return BigNumber.from(0); 14 | } 15 | let intermediate = magnitude; 16 | let i = 0; 17 | while ((intermediate * 10) <= Number.MAX_SAFE_INTEGER) { 18 | intermediate *= 10; 19 | i++; // eslint-disable-line no-plusplus 20 | } 21 | if (Math.trunc(intermediate) !== intermediate) { 22 | throw new Error('!'); 23 | } 24 | return BigNumber.from(intermediate).mul(BigNumber.from(10).pow(BigNumber.from(18 - i))).mul(BigNumber.from(sign)); 25 | }; 26 | 27 | export const sumOf = (...values: BigNumber[]): BigNumber => 28 | values.reduce((a: BigNumber, b: BigNumber) => a.add(b), BigNumber.from(0)); 29 | 30 | export const formatUsdc = (wei: BigNumberish): string => 31 | `${(BigNumber.from(wei).toNumber() / 1e6).toFixed(6).replace(/\.(\d{2})(\d{4})/, '.$1,$2')} USDC`; 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 | export const findContractEventArgs = (events: ContractReceipt['events'], name: string): T => { 35 | assert(events !== undefined); 36 | const event = events.find(({ event }) => event === name); 37 | assert(event); 38 | assert(event.args); 39 | return event.args as unknown as T; 40 | }; 41 | 42 | export interface UserCommitment { 43 | virtualFloorId: BigNumber; 44 | committer: string; 45 | outcomeIndex: BigNumber; 46 | timeslot: BigNumber; 47 | amount: BigNumber; 48 | beta_e18: BigNumber; 49 | tokenId: BigNumber; 50 | } 51 | 52 | enum VirtualFloorResolutionType { 53 | 'NoWinners', 54 | 'AllWinners', 55 | 'SomeWinners' 56 | } 57 | 58 | export interface VirtualFloorResolution { 59 | virtualFloorId: BigNumber; 60 | winningOutcomeIndex: BigNumber; 61 | resolutionType: VirtualFloorResolutionType; 62 | winnerProfits: BigNumber; 63 | platformFeeAmount: BigNumber; 64 | creatorFeeAmount: BigNumber; 65 | } 66 | 67 | export const findUserCommitmentEventArgs = (events: ContractReceipt['events']): UserCommitment => { 68 | return findContractEventArgs(events, 'UserCommitment'); 69 | }; 70 | 71 | export const findVFResolutionEventArgs = (events: ContractReceipt['events']): VirtualFloorResolution => { 72 | return findContractEventArgs(events, 'VirtualFloorResolution'); 73 | }; 74 | 75 | export const DUMMY_METADATA: RoomEventInfo = { 76 | category: 'sports', 77 | subcategory: 'football', 78 | title: 'Finland vs. Argentina', 79 | description: 'Finland vs. Argentina FIFA 2022 world cup final', 80 | isListed: false, 81 | opponents: [ 82 | { title: 'Finland', image: 'https://upload.wikimedia.org/wikipedia/commons/3/31/Huuhkajat_logo.svg' }, 83 | { title: 'Argentina', image: 'https://upload.wikimedia.org/wikipedia/en/c/c1/Argentina_national_football_team_logo.svg' } 84 | ], 85 | outcomes: [ 86 | { title: 'Finland win' }, 87 | { title: 'Argentina win' }, 88 | { title: 'Tie' } 89 | ], 90 | resultSources: [ 91 | { title: 'Official FIFA result page', url: 'http://fifa.com/argentina-vs-finland' } 92 | ], 93 | discordChannelId: '123456789', 94 | extraData: '0x', 95 | }; 96 | 97 | export const generateRandomVirtualFloorId = () => 98 | BigNumber.from(ethers.utils.hexlify(ethers.utils.randomBytes(8))).shl(5 * 8); 99 | 100 | export const ENCODED_DUMMY_METADATA = encodeVirtualFloorMetadata(DUMMY_METADATA); 101 | 102 | export const timestampMinuteCeil = (timestamp: number) => Math.ceil(timestamp / 60) * 60; 103 | 104 | export const toTimestamp = (datetime: string): number => BigNumber.from(new Date(datetime).getTime() / 1000).toNumber(); 105 | 106 | export function tokenIdOf({ vfId, outcomeIndex, timeslot }: { vfId: BigNumberish; outcomeIndex: number; timeslot: BigNumberish }): BigNumber { 107 | return BigNumber.from(ethers.utils.solidityPack( 108 | ['uint216', 'uint8', 'uint32'], 109 | [BigNumber.from(vfId).shr((1 + 4) * 8), outcomeIndex, timeslot] 110 | )); 111 | } 112 | 113 | export const $ = (dollars: BigNumberish, millionths: BigNumberish = 0): BigNumber => 114 | BigNumber.from(1000000) 115 | .mul(dollars) 116 | .add(millionths); 117 | 118 | export const UNSPECIFIED_COMMITMENT_DEADLINE = 0; 119 | -------------------------------------------------------------------------------- /platform/scripts/create-test-vf.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@ethersproject/bignumber'; 2 | import assert from 'assert'; 3 | import { ethers } from 'hardhat'; 4 | import { DUMMY_METADATA, UNSPECIFIED_COMMITMENT_DEADLINE } from '../helpers'; 5 | import { DoubleDice__factory, DummyUSDCoin__factory, encodeVirtualFloorMetadata } from '../lib/contracts'; 6 | import { validateRoomEventInfo } from '../lib/metadata'; 7 | 8 | const TOKEN_CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; 9 | const PLATFORM_CONTRACT_ADDRESS = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'; 10 | 11 | async function main() { 12 | 13 | const roomEventInfo = DUMMY_METADATA; 14 | 15 | assert(validateRoomEventInfo(roomEventInfo)); 16 | 17 | const [owner, user1, user2] = await ethers.getSigners(); 18 | 19 | console.log(owner.address); 20 | 21 | const platform = new DoubleDice__factory(owner).attach(PLATFORM_CONTRACT_ADDRESS); 22 | 23 | const token = new DummyUSDCoin__factory(owner).attach(TOKEN_CONTRACT_ADDRESS); 24 | 25 | // const { timestamp } = await ethers.provider.getBlock('latest'); 26 | 27 | const unroundedTimestamp = Math.floor(Date.now() / 1000); 28 | const timestamp = unroundedTimestamp - unroundedTimestamp % 60; 29 | 30 | console.log(`timestamp = ${timestamp}`); 31 | 32 | const vfId = BigNumber.from(ethers.utils.hexlify(ethers.utils.randomBytes(8))).shl(5 * 8); 33 | console.log(`vfId = ${vfId}`); 34 | 35 | const tOpen = timestamp + 0 * 86400; 36 | const tClose = timestamp + 1 * 86400; 37 | const tResolve = timestamp + 2 * 86400; 38 | 39 | await (await platform.createVirtualFloor({ 40 | virtualFloorId: vfId, 41 | betaOpen_e18: 100_000000_000000_000000n, // = 100.0 42 | creationFeeRate_e18: 12500_000000_000000n, // = 0.0125 = 1.25% 43 | tOpen, 44 | tClose, 45 | tResolve, 46 | nOutcomes: roomEventInfo.outcomes.length, 47 | paymentToken: TOKEN_CONTRACT_ADDRESS, 48 | bonusAmount: 0, 49 | optionalMinCommitmentAmount: 0, 50 | optionalMaxCommitmentAmount: 0, 51 | metadata: encodeVirtualFloorMetadata(DUMMY_METADATA), 52 | })).wait(); 53 | 54 | const amt = 100_000000_000000_000000n; 55 | await (await token.mint(user1.address, amt)).wait(); 56 | await (await token.mint(user2.address, amt)).wait(); 57 | 58 | await (await token.connect(user1).increaseAllowance(platform.address, amt)).wait(); 59 | await (await token.connect(user2).increaseAllowance(platform.address, amt)).wait(); 60 | 61 | console.log(`balanceOf(1) = ${await token.balanceOf(user1.address)}`); 62 | console.log(`balanceOf(2) = ${await token.balanceOf(user2.address)}`); 63 | 64 | const { events: events1 } = await (await platform.connect(user1).commitToVirtualFloor(vfId, 0, 100000_000000_000000n, UNSPECIFIED_COMMITMENT_DEADLINE)).wait(); 65 | const { events: events2 } = await (await platform.connect(user1).commitToVirtualFloor(vfId, 1, 200000_000000_000000n, UNSPECIFIED_COMMITMENT_DEADLINE)).wait(); 66 | const { events: events3 } = await (await platform.connect(user2).commitToVirtualFloor(vfId, 1, 300000_000000_000000n, UNSPECIFIED_COMMITMENT_DEADLINE)).wait(); 67 | const { events: events4 } = await (await platform.connect(user2).commitToVirtualFloor(vfId, 2, 400000_000000_000000n, UNSPECIFIED_COMMITMENT_DEADLINE)).wait(); 68 | 69 | const { args: { id: id1 } } = events1!.find(({ event }) => event === 'TransferSingle') as any; 70 | const { args: { id: id2 } } = events2!.find(({ event }) => event === 'TransferSingle') as any; 71 | const { args: { id: id3 } } = events3!.find(({ event }) => event === 'TransferSingle') as any; 72 | const { args: { id: id4 } } = events4!.find(({ event }) => event === 'TransferSingle') as any; 73 | 74 | assert(BigNumber.isBigNumber(id1)); 75 | assert(BigNumber.isBigNumber(id2)); 76 | 77 | assert(id2.eq(id3)); 78 | 79 | // ToDo: Use evm_setNextBlockTimestamp as soon as we move to hardhat in Docker configuration, 80 | // or as soon as ganache-cli supports it 81 | await ethers.provider.send('evm_mine', []); 82 | const { timestamp: now } = await ethers.provider.getBlock('latest'); 83 | await ethers.provider.send('evm_increaseTime', [tClose - now]); 84 | await (await platform.connect(user1).safeTransferFrom(user1.address, user2.address, id2, 25000_000000_000000n, '0x')).wait(); 85 | 86 | console.log({ 87 | id1: id1.toString(), 88 | id2: id2.toString(), 89 | id3: id3.toString(), 90 | id4: id4.toString(), 91 | }); 92 | } 93 | 94 | main() 95 | .then(() => process.exit(0)) 96 | .catch(error => { 97 | console.error(error); 98 | process.exit(1); 99 | }); 100 | -------------------------------------------------------------------------------- /platform/test/FixedPointTypes.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { 4 | FixedPointTypesMock, 5 | FixedPointTypesMock__factory 6 | } from '../lib/contracts'; 7 | 8 | describe('FixedPointTypes', function () { 9 | 10 | let lib: FixedPointTypesMock; 11 | 12 | before(async function () { 13 | const [signer] = await ethers.getSigners(); 14 | lib = await new FixedPointTypesMock__factory(signer).deploy(); 15 | await lib.deployed(); 16 | }); 17 | 18 | it('add', async function () { 19 | expect(await lib.add(100_000000_000000_000000n, 23_450000_000000_000000n)).to.eq(123_450000_000000_000000n); 20 | }); 21 | 22 | it('sub', async function () { 23 | expect(await lib.sub(123_450000_000000_000000n, 23_450000_000000_000000n)).to.eq(100_000000_000000_000000n); 24 | }); 25 | 26 | it('mul0', async function () { 27 | expect(await lib.mul0(1_230000_000000_000000n, 3)).to.eq(3_690000_000000_000000n); 28 | }); 29 | 30 | it('div0', async function () { 31 | expect(await lib.div0(1_000000_000000_000000n, 3)).to.eq(333333_333333_333333n); 32 | }); 33 | 34 | it('divToUint256', async function () { 35 | expect(await lib.divToUint256(200_000000_000000_000000n, 3_000000_000000_000000n)).to.eq(66); 36 | }); 37 | 38 | it('floorToUint256', async function () { 39 | expect(await lib.floorToUint256(987_654321_000000_000000n)).to.eq(987); 40 | }); 41 | 42 | it('eq', async function () { 43 | expect(await lib.eq(0, 0)).to.be.true; 44 | expect(await lib.eq(0, 1)).to.be.false; 45 | expect(await lib.eq(1, 0)).to.be.false; 46 | expect(await lib.eq(7_000000_000000_000000n, 7_000000_000000_000000n)).to.be.true; 47 | expect(await lib.eq(7_000000_000000_000000n, 7_000000_000000_000001n)).to.be.false; 48 | expect(await lib.eq(7_000000_000000_000001n, 7_000000_000000_000000n)).to.be.false; 49 | }); 50 | 51 | it('lte', async function () { 52 | expect(await lib.lte(0, 0)).to.be.true; 53 | expect(await lib.lte(0, 1)).to.be.true; 54 | expect(await lib.lte(1, 0)).to.be.false; 55 | expect(await lib.lte(7_000000_000000_000000n, 7_000000_000000_000000n)).to.be.true; 56 | expect(await lib.lte(7_000000_000000_000000n, 7_000000_000000_000001n)).to.be.true; 57 | expect(await lib.lte(7_000000_000000_000001n, 7_000000_000000_000000n)).to.be.false; 58 | }); 59 | 60 | it('gte', async function () { 61 | expect(await lib.gte(0, 0)).to.be.true; 62 | expect(await lib.gte(0, 1)).to.be.false; 63 | expect(await lib.gte(1, 0)).to.be.true; 64 | expect(await lib.gte(7_000000_000000_000000n, 7_000000_000000_000000n)).to.be.true; 65 | expect(await lib.gte(7_000000_000000_000000n, 7_000000_000000_000001n)).to.be.false; 66 | expect(await lib.gte(7_000000_000000_000001n, 7_000000_000000_000000n)).to.be.true; 67 | }); 68 | 69 | it('toUFixed16x4', async function () { 70 | expect(await lib.toUFixed16x4(234500_000000_000000n)).to.eq(2345); 71 | expect(await lib.toUFixed16x4(1_234500_000000_000000n)).to.eq(1_2345); 72 | await expect(lib.toUFixed16x4(1_234560_000000_000000n)).to.be.revertedWith('UFixed16x4LossOfPrecision(1234560000000000000)'); 73 | expect(await lib.toUFixed16x4(6_553500_000000_000000n)).to.eq(6_5535); 74 | await expect(lib.toUFixed16x4(6_553510_000000_000000n)).to.be.revertedWith('UFixed16x4LossOfPrecision(6553510000000000000)'); 75 | await expect(lib.toUFixed16x4(6_553600_000000_000000n)).to.be.revertedWith('SafeCast: value doesn\'t fit in 16 bits'); 76 | }); 77 | 78 | it('toUFixed32x6', async function () { 79 | expect(await lib.toUFixed32x6(234567_000000_000000n)).to.eq(234567); 80 | expect(await lib.toUFixed32x6(1_234567_000000_000000n)).to.eq(1_234567); 81 | await expect(lib.toUFixed32x6(1_234567_800000_000000n)).to.be.revertedWith('UFixed32x6LossOfPrecision(1234567800000000000)'); 82 | expect(await lib.toUFixed32x6(4294_967295_000000_000000n)).to.eq(4294_967295); 83 | await expect(lib.toUFixed32x6(4294_967295_100000_000000n)).to.be.revertedWith('UFixed32x6LossOfPrecision(4294967295100000000000)'); 84 | await expect(lib.toUFixed32x6(4294_967296_000000_000000n)).to.be.revertedWith('SafeCast: value doesn\'t fit in 32 bits'); 85 | }); 86 | 87 | it('toUFixed256x18(uint256)', async function () { 88 | expect(await lib.toUFixed256x18__fromUint256(123)).to.eq(123_000000_000000_000000n); 89 | }); 90 | 91 | it('toUFixed256x18(UFixed16x4)', async function () { 92 | expect(await lib.toUFixed256x18__fromUFixed16x4(1_2345)).to.eq(1_234500_000000_000000n); 93 | }); 94 | 95 | it('toUFixed256x18(UFixed32x6)', async function () { 96 | expect(await lib.toUFixed256x18__fromUFixed32x6(123_456789)).to.eq(123_456789_000000_000000n); 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /platform/helpers/contract-helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigNumber, 3 | BigNumberish, 4 | ContractReceipt, 5 | Signer 6 | } from 'ethers'; 7 | import { ethers } from 'hardhat'; 8 | import { 9 | findUserCommitmentEventArgs, 10 | findVFResolutionEventArgs, 11 | SignerWithAddress, 12 | UserCommitment, 13 | VirtualFloorResolution 14 | } from '.'; 15 | import { 16 | BaseDoubleDice, 17 | DoubleDice, 18 | DummyUSDCoin, 19 | DummyWrappedBTC 20 | } from '../lib/contracts'; 21 | import { EvmHelper } from './evm'; 22 | 23 | type AddressOrSigner = string | SignerWithAddress; 24 | 25 | export const toAddress = (addressOrSigner: AddressOrSigner) => typeof addressOrSigner === 'string' ? addressOrSigner : addressOrSigner.address; 26 | 27 | // ToDo: Move into Helper class, use provider supplied to its constructor 28 | const evm = new EvmHelper(ethers.provider); 29 | 30 | export class DoubleDicePlatformHelper { 31 | constructor(private contract: DoubleDice) { } 32 | 33 | balanceOf(addressOrSigner: string, tokenId: string): Promise { 34 | return this.contract.balanceOf(addressOrSigner, tokenId); 35 | } 36 | 37 | async mintTokensForUser({ 38 | token, 39 | ownerSigner, 40 | userAddress, 41 | amount, 42 | }: { 43 | token: DummyUSDCoin | DummyWrappedBTC; 44 | ownerSigner: SignerWithAddress; 45 | userAddress: string; 46 | amount: BigNumber; 47 | }) { 48 | return await ( 49 | await token.connect(ownerSigner).mint(userAddress, amount) 50 | ).wait(); 51 | } 52 | async mintTokenAndGiveAllowanceToContract({ 53 | token, 54 | ownerSigner, 55 | usersSigner, 56 | mintAmount, 57 | allowanceAmount, 58 | contractAddress, 59 | }: { 60 | token: DummyUSDCoin | DummyWrappedBTC; 61 | ownerSigner: SignerWithAddress; 62 | usersSigner: SignerWithAddress[]; 63 | mintAmount: BigNumber; 64 | allowanceAmount: BigNumber; 65 | contractAddress: string; 66 | }) { 67 | for (const userSigner of usersSigner) { 68 | await ( 69 | await token.connect(userSigner).approve(contractAddress, allowanceAmount) 70 | ).wait(); 71 | 72 | await ( 73 | await token.connect(ownerSigner).mint(toAddress(userSigner), mintAmount) 74 | ).wait(); 75 | } 76 | } 77 | 78 | // async createVirtualFloor( 79 | // virtualFloorCreationParams: VirtualFloorCreationParamsStruct 80 | // ) { 81 | // return await ( 82 | // await this.contract.createVirtualFloor(virtualFloorCreationParams) 83 | // ).wait(); 84 | // } 85 | 86 | async commitToVirtualFloor( 87 | virtualFloorId: BigNumberish, 88 | outcomeIndex: number, 89 | userSigner: SignerWithAddress, 90 | amount: BigNumberish, 91 | deadline: BigNumberish, 92 | ): Promise { 93 | const { events } = await ( 94 | await this.contract 95 | .connect(userSigner) 96 | .commitToVirtualFloor(virtualFloorId, outcomeIndex, amount, deadline) 97 | ).wait(); 98 | 99 | return (findUserCommitmentEventArgs( 100 | events 101 | ) as unknown) as UserCommitment; 102 | } 103 | 104 | async resolveVirtualFloor( 105 | virtualFloorId: BigNumberish, 106 | outcomeIndex: number, 107 | ownerSigner: SignerWithAddress 108 | ): Promise { 109 | const { events } = await ( 110 | await this.contract 111 | .connect(ownerSigner) 112 | .setResult(virtualFloorId, outcomeIndex) 113 | ).wait(); 114 | 115 | return (findVFResolutionEventArgs( 116 | events 117 | ) as unknown) as VirtualFloorResolution; 118 | } 119 | 120 | async setResultThenLaterConfirmUnchallengedResult(signer: Signer, ...[vfId, ...otherArgs]: Parameters): Promise<[VirtualFloorResolution, ContractReceipt, ContractReceipt]> { 121 | const rx1 = await (await this.contract.connect(signer).setResult(vfId, ...otherArgs)).wait(); 122 | 123 | // ToDo: Contract should store tChallengeMax directly, instead of storing setTimestamp 124 | const { tResultChallengeMax } = await this.contract.resolutions(vfId); 125 | const CHALLENGE_WINDOW_DURATION = await this.contract.CHALLENGE_WINDOW_DURATION(); 126 | const tChallengeMax = BigNumber.from(tResultChallengeMax).add(CHALLENGE_WINDOW_DURATION); 127 | 128 | await evm.setNextBlockTimestamp(tChallengeMax); 129 | 130 | const rx2 = await (await this.contract.connect(signer).confirmUnchallengedResult(vfId)).wait(); 131 | 132 | const vfResolutionEvent = findVFResolutionEventArgs(rx2.events) as unknown as VirtualFloorResolution; 133 | 134 | return [vfResolutionEvent, rx1, rx2]; 135 | } 136 | 137 | async claimPayouts(userSigner: Signer, ...args: Parameters): Promise { 138 | return await (await this.contract.connect(userSigner).claimPayouts(...args)).wait(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /platform/subgraph/assemblyscript/entities.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigDecimal, 3 | BigInt 4 | } from '@graphprotocol/graph-ts'; 5 | import { 6 | OutcomeTimeslot, 7 | User, 8 | UserOutcome, 9 | UserOutcomeTimeslot 10 | } from '../../generated/schema'; 11 | 12 | 13 | interface Entity { 14 | save(): void 15 | } 16 | 17 | type LoadEntity = (id: string) => T | null 18 | 19 | export function createNewEntity(load: LoadEntity, id: string): T { 20 | let entity = load(id); 21 | assert(entity == null, `createNewEntity: Expected entity ${id} to NOT already exist`); 22 | entity = instantiate(id); 23 | entity.save(); 24 | return entity; 25 | } 26 | 27 | export function loadExistentEntity(load: LoadEntity, id: string): T { 28 | return assert(load(id), `loadExistentEntity: Expected entity ${id} to already exist`); 29 | } 30 | 31 | // ToDo: Ideally this would return { entity, isNew }, 32 | // so that caller could use isNew to run some code only the first time. 33 | export function loadOrCreateEntity(load: LoadEntity, id: string): T { 34 | let entity = load(id); 35 | if (entity == null) { 36 | entity = instantiate(id); 37 | entity.save(); 38 | } 39 | return entity; 40 | } 41 | 42 | function assertFieldEqual(entityName: string, id: string, fieldName: string, loadedFieldValue: T, expectedValue: T): void { 43 | // Note: Important to use == until === becomes supported 44 | assert(loadedFieldValue == expectedValue, `${entityName}(${id}).${fieldName} == ${loadedFieldValue} != ${expectedValue}`); 45 | } 46 | 47 | export function assertOutcomeTimeslotEntity(id: string, 48 | outcomeEntityId: string, 49 | timeslot: BigInt, 50 | tokenId: BigInt, 51 | beta: BigDecimal, 52 | ): OutcomeTimeslot { 53 | const loaded = OutcomeTimeslot.load(id); 54 | if (loaded == null) { 55 | const created = new OutcomeTimeslot(id); 56 | { 57 | created.outcome = outcomeEntityId; 58 | created.timeslot = timeslot; 59 | created.tokenId = tokenId; 60 | created.beta = beta; 61 | } 62 | created.save() 63 | return created; 64 | } else { 65 | { 66 | assertFieldEqual('OutcomeTimeslot', id, 'outcome', loaded.outcome, outcomeEntityId); 67 | assertFieldEqual('OutcomeTimeslot', id, 'timeslot', loaded.timeslot, timeslot); 68 | assertFieldEqual('OutcomeTimeslot', id, 'tokenId', loaded.tokenId, tokenId); 69 | assertFieldEqual('OutcomeTimeslot', id, 'beta', loaded.beta, beta); 70 | } 71 | return loaded; 72 | } 73 | } 74 | 75 | export function assertUserEntity(id: string): User { 76 | const loaded = User.load(id); 77 | if (loaded == null) { 78 | const created = new User(id); 79 | { 80 | } 81 | created.save() 82 | return created; 83 | } else { 84 | { 85 | } 86 | return loaded; 87 | } 88 | } 89 | 90 | export function assertUserOutcomeEntity(id: string, 91 | userEntityId: string, 92 | outcomeEntityId: string, 93 | ): UserOutcome { 94 | const loaded = UserOutcome.load(id); 95 | if (loaded == null) { 96 | const created = new UserOutcome(id); 97 | { 98 | created.user = userEntityId; 99 | created.outcome = outcomeEntityId; 100 | } 101 | created.save() 102 | return created; 103 | } else { 104 | { 105 | assertFieldEqual('UserOutcome', id, 'user', loaded.user, userEntityId); 106 | assertFieldEqual('UserOutcome', id, 'outcome', loaded.outcome, outcomeEntityId); 107 | } 108 | return loaded; 109 | } 110 | } 111 | 112 | export function assertUserOutcomeTimeslotEntity(id: string, 113 | userEntityId: string, 114 | outcomeEntityId: string, 115 | timeslot: BigInt, 116 | userOutcomeEntityId: string, 117 | outcomeTimeslotEntityId: string, 118 | ): UserOutcomeTimeslot { 119 | const loaded = UserOutcomeTimeslot.load(id); 120 | if (loaded == null) { 121 | const created = new UserOutcomeTimeslot(id); 122 | { 123 | created.user = userEntityId; 124 | created.outcome = outcomeEntityId; 125 | created.timeslot = timeslot; // ToDo: Deprecate 126 | created.userOutcome = userOutcomeEntityId; 127 | created.outcomeTimeslot = outcomeTimeslotEntityId; 128 | } 129 | created.save() 130 | return created; 131 | } else { 132 | { 133 | assertFieldEqual('UserOutcomeTimeslot', id, 'user', loaded.user, userEntityId); 134 | assertFieldEqual('UserOutcomeTimeslot', id, 'outcome', loaded.outcome, outcomeEntityId); 135 | assertFieldEqual('UserOutcomeTimeslot', id, 'timeslot', loaded.timeslot, timeslot); // ToDo: Deprecate 136 | assertFieldEqual('UserOutcomeTimeslot', id, 'userOutcome', loaded.userOutcome, userOutcomeEntityId); 137 | assertFieldEqual('UserOutcomeTimeslot', id, 'outcomeTimeslot', loaded.outcomeTimeslot, outcomeTimeslotEntityId); 138 | } 139 | return loaded; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /platform/contracts/library/FixedPointTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; 6 | 7 | type UFixed32x6 is uint32; 8 | 9 | type UFixed16x4 is uint16; 10 | 11 | type UFixed256x18 is uint256; 12 | 13 | UFixed256x18 constant UFIXED256X18_ONE = UFixed256x18.wrap(1e18); 14 | 15 | 16 | error UFixed16x4LossOfPrecision(UFixed256x18 value); 17 | 18 | error UFixed32x6LossOfPrecision(UFixed256x18 value); 19 | 20 | 21 | /// @notice The primary fixed-point type is UFixed256x18, 22 | /// but some conversions to UFixed32x6 and UFixed16x4 are also provided, 23 | /// as these are used on the main contract. 24 | library FixedPointTypes { 25 | 26 | using SafeCastUpgradeable for uint256; 27 | using FixedPointTypes for UFixed16x4; 28 | using FixedPointTypes for UFixed32x6; 29 | using FixedPointTypes for UFixed256x18; 30 | 31 | function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) { 32 | return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b)); 33 | } 34 | 35 | function sub(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) { 36 | return UFixed256x18.wrap(UFixed256x18.unwrap(a) - UFixed256x18.unwrap(b)); 37 | } 38 | 39 | /// @dev e.g. 1.230000_000000_000000 * 3 = 3.690000_000000_000000 40 | /// Named `mul0` because unlike `add` and `sub`, `b` is `UFixed256x0`, not `UFixed256x18` 41 | function mul0(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) { 42 | return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b); 43 | } 44 | 45 | function div0(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) { 46 | return UFixed256x18.wrap(UFixed256x18.unwrap(a) / b); 47 | } 48 | 49 | /// @dev More efficient implementation of (hypothetical) `value.div(b).toUint256()` 50 | /// e.g. 200.000000_000000_000000 / 3.000000_000000_000000 = 33 51 | function divToUint256(UFixed256x18 a, UFixed256x18 b) internal pure returns (uint256) { 52 | return UFixed256x18.unwrap(a) / UFixed256x18.unwrap(b); 53 | } 54 | 55 | /// @dev More efficient implementation of (hypothetical) `value.floor().toUint256()` 56 | /// e.g. 987.654321_000000_000000 => 987 57 | function floorToUint256(UFixed256x18 value) internal pure returns (uint256) { 58 | return UFixed256x18.unwrap(value) / 1e18; 59 | } 60 | 61 | 62 | function eq(UFixed256x18 a, UFixed256x18 b) internal pure returns (bool) { 63 | return UFixed256x18.unwrap(a) == UFixed256x18.unwrap(b); 64 | } 65 | 66 | function gte(UFixed256x18 a, UFixed256x18 b) internal pure returns (bool) { 67 | return UFixed256x18.unwrap(a) >= UFixed256x18.unwrap(b); 68 | } 69 | 70 | function lte(UFixed256x18 a, UFixed256x18 b) internal pure returns (bool) { 71 | return UFixed256x18.unwrap(a) <= UFixed256x18.unwrap(b); 72 | } 73 | 74 | 75 | /// @notice e.g. 1.234500_000000_000000 => 1.2345 76 | /// Reverts if input is too large to fit in output-type, 77 | /// or if conversion would lose precision, e.g. 1.234560_000000_000000 will revert. 78 | function toUFixed16x4(UFixed256x18 value) internal pure returns (UFixed16x4 converted) { 79 | converted = UFixed16x4.wrap((UFixed256x18.unwrap(value) / 1e14).toUint16()); 80 | if (!(converted.toUFixed256x18().eq(value))) revert UFixed16x4LossOfPrecision(value); 81 | } 82 | 83 | /// @notice e.g. 123.456789_000000_000000 => 123.456789 84 | /// Reverts if input is too large to fit in output-type, 85 | /// or if conversion would lose precision, e.g. 123.456789_100000_000000 will revert. 86 | function toUFixed32x6(UFixed256x18 value) internal pure returns (UFixed32x6 converted) { 87 | converted = UFixed32x6.wrap((UFixed256x18.unwrap(value) / 1e12).toUint32()); 88 | if (!(converted.toUFixed256x18().eq(value))) revert UFixed32x6LossOfPrecision(value); 89 | } 90 | 91 | /// @notice e.g. 123 => 123.000000_000000_000000 92 | /// Reverts if input is too large to fit in output-type. 93 | function toUFixed256x18(uint256 value) internal pure returns (UFixed256x18) { 94 | return UFixed256x18.wrap(value * 1e18); 95 | } 96 | 97 | /// @notice e.g. 1.2345 => 1.234500_000000_000000 98 | /// Input always fits in output-type. 99 | function toUFixed256x18(UFixed16x4 value) internal pure returns (UFixed256x18 converted) { 100 | unchecked { // because type(uint16).max * 1e14 <= type(uint256).max 101 | return UFixed256x18.wrap(uint256(UFixed16x4.unwrap(value)) * 1e14); 102 | } 103 | } 104 | 105 | /// @notice e.g. 123.456789 => 123.456789_000000_000000 106 | /// Input always fits in output-type. 107 | function toUFixed256x18(UFixed32x6 value) internal pure returns (UFixed256x18 converted) { 108 | unchecked { // because type(uint32).max * 1e12 <= type(uint256).max 109 | return UFixed256x18.wrap(uint256(UFixed32x6.unwrap(value)) * 1e12); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /platform/contracts/library/VirtualFloors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | 3 | pragma solidity 0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 6 | 7 | import "../BaseDoubleDice.sol"; 8 | import "../interface/IDoubleDice.sol"; 9 | import "./FixedPointTypes.sol"; 10 | 11 | uint256 constant _MIN_POSSIBLE_COMMITMENT_AMOUNT = 1; 12 | uint256 constant _MAX_POSSIBLE_COMMITMENT_AMOUNT = type(uint256).max; 13 | 14 | library VirtualFloors { 15 | 16 | using FixedPointTypes for UFixed256x18; 17 | using FixedPointTypes for UFixed32x6; 18 | using SafeERC20Upgradeable for IERC20Upgradeable; 19 | using VirtualFloors for VirtualFloor; 20 | 21 | function state(VirtualFloor storage vf) internal view returns (VirtualFloorState) { 22 | VirtualFloorInternalState _internalState = vf._internalState; 23 | if (_internalState == VirtualFloorInternalState.None) { 24 | return VirtualFloorState.None; 25 | } else if (_internalState == VirtualFloorInternalState.Active) { 26 | if (block.timestamp < vf.tClose) { 27 | if (vf.nonzeroOutcomeCount >= 2) { 28 | return VirtualFloorState.Active_Open_ResolvableLater; 29 | } else { 30 | return VirtualFloorState.Active_Open_MaybeResolvableNever; 31 | } 32 | } else { 33 | if (vf.nonzeroOutcomeCount >= 2) { 34 | if (block.timestamp < vf.tResolve) { 35 | return VirtualFloorState.Active_Closed_ResolvableLater; 36 | } else { 37 | return VirtualFloorState.Active_Closed_ResolvableNow; 38 | } 39 | } else { 40 | return VirtualFloorState.Active_Closed_ResolvableNever; 41 | } 42 | } 43 | } else if (_internalState == VirtualFloorInternalState.Claimable_Payouts) { 44 | return VirtualFloorState.Claimable_Payouts; 45 | } else if (_internalState == VirtualFloorInternalState.Claimable_Refunds_ResolvedNoWinners) { 46 | return VirtualFloorState.Claimable_Refunds_ResolvedNoWinners; 47 | } else if (_internalState == VirtualFloorInternalState.Claimable_Refunds_ResolvableNever) { 48 | return VirtualFloorState.Claimable_Refunds_ResolvableNever; 49 | } else /*if (_internalState == VirtualFloorInternalState.Claimable_Refunds_Flagged)*/ { 50 | assert(_internalState == VirtualFloorInternalState.Claimable_Refunds_Flagged); // Ensure all enum values have been handled. 51 | return VirtualFloorState.Claimable_Refunds_Flagged; 52 | } 53 | } 54 | 55 | /// @dev Compare: 56 | /// 1. (((tClose - t) * (betaOpen - 1)) / (tClose - tOpen)) * amount 57 | /// 2. (((tClose - t) * (betaOpen - 1) * amount) / (tClose - tOpen)) 58 | /// (2) has less rounding error than (1), but then the *precise* effective beta used in the computation might not 59 | /// have a uint256 representation. 60 | /// Therefore we sacrifice some (miniscule) rounding error to gain computation reproducibility. 61 | function betaOf(VirtualFloor storage vf, uint256 t) internal view returns (UFixed256x18) { 62 | UFixed256x18 betaOpenMinusBetaClose = vf.betaOpenMinusBetaClose.toUFixed256x18(); 63 | return _BETA_CLOSE.add(betaOpenMinusBetaClose.mul0(vf.tClose - t).div0(vf.tClose - vf.tOpen)); 64 | } 65 | 66 | function totalCommitmentsToAllOutcomesPlusBonus(VirtualFloor storage vf) internal view returns (uint256 total) { 67 | total = vf.bonusAmount; 68 | for (uint256 i = 0; i < vf.nOutcomes; i++) { 69 | total += vf.outcomeTotals[i].amount; 70 | } 71 | } 72 | 73 | function minMaxCommitmentAmounts(VirtualFloor storage vf) internal view returns (uint256 min, uint256 max) { 74 | min = vf._optionalMinCommitmentAmount; 75 | max = vf._optionalMaxCommitmentAmount; 76 | if (min == UNSPECIFIED_ZERO) { 77 | min = _MIN_POSSIBLE_COMMITMENT_AMOUNT; 78 | } 79 | if (max == UNSPECIFIED_ZERO) { 80 | max = _MAX_POSSIBLE_COMMITMENT_AMOUNT; 81 | } 82 | } 83 | 84 | /// @dev Equivalent to state == Active_Open_ResolvableLater || state == Active_Open_MaybeResolvableNever, 85 | /// but ~300 gas cheaper. 86 | function isOpen(VirtualFloor storage vf) internal view returns (bool) { 87 | return vf._internalState == VirtualFloorInternalState.Active && block.timestamp < vf.tClose; 88 | } 89 | 90 | function isClaimableRefunds(VirtualFloor storage vf) internal view returns (bool) { 91 | return vf._internalState == VirtualFloorInternalState.Claimable_Refunds_ResolvedNoWinners 92 | || vf._internalState == VirtualFloorInternalState.Claimable_Refunds_ResolvableNever 93 | || vf._internalState == VirtualFloorInternalState.Claimable_Refunds_Flagged; 94 | } 95 | 96 | function refundBonusAmount(VirtualFloor storage vf) internal { 97 | if (vf.bonusAmount > 0) { 98 | vf.paymentToken.safeTransfer(vf.creator, vf.bonusAmount); 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /platform/lib/graph.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export * from './generated/graphql'; 4 | 5 | import assert from 'assert'; 6 | import { BigNumber as BigDecimal } from 'bignumber.js'; 7 | import { BigNumber as EthersBigInteger } from 'ethers'; 8 | import { 9 | Outcome as OutcomeEntity, 10 | VirtualFloor as VirtualFloorEntity, 11 | VirtualFloorState as VirtualFloorEntityState 12 | } from './generated/graphql'; 13 | 14 | export enum VirtualFloorClaimType { 15 | Payouts, 16 | Refunds 17 | } 18 | 19 | export interface PreparedClaim { 20 | claimType: VirtualFloorClaimType; 21 | tokenIds: EthersBigInteger[]; 22 | totalClaimAmount: BigDecimal; 23 | } 24 | 25 | const MISSING = undefined; 26 | const BLANK = null; // e.g. winnerOutome is "blank" when VF is not yet resolved 27 | 28 | export const prepareVirtualFloorClaim = (vf: Partial): PreparedClaim | null => { 29 | // Assert that the fields have been included in the query 30 | assert(vf.state !== MISSING, 'Missing field: VirtualFloor.state'); 31 | assert(vf.winningOutcome !== MISSING, 'Missing field: VirtualFloor.winningOutcome'); 32 | assert(vf.winnerProfits !== MISSING, 'Missing field: VirtualFloor.winnerProfits'); 33 | 34 | switch (vf.state) { 35 | case VirtualFloorEntityState.Claimable_Payouts: { 36 | // Since they are not missing, they must be non-blank since 37 | // on the Graph they are always set for a VF resolved with winners 38 | assert(vf.winningOutcome !== BLANK); 39 | assert(vf.winnerProfits !== BLANK); 40 | 41 | assert(vf.winningOutcome.totalWeightedSupply !== MISSING, 'Missing field: VirtualFloor.winningOutcome.totalWeightedSupply'); 42 | 43 | const winnerProfits = new BigDecimal(vf.winnerProfits); 44 | const winningOutcomeTotalAmountTimesBeta = new BigDecimal(vf.winningOutcome.totalWeightedSupply); 45 | 46 | assert(vf.winningOutcome.userOutcomes !== MISSING); 47 | assert(vf.winningOutcome.userOutcomes.length === 0 || vf.winningOutcome.userOutcomes.length === 1); 48 | 49 | if (vf.winningOutcome.userOutcomes.length === 1) { 50 | 51 | const [userOutcome] = vf.winningOutcome.userOutcomes; 52 | 53 | assert(userOutcome.totalBalance !== MISSING); 54 | assert(userOutcome.totalWeightedBalance !== MISSING); 55 | 56 | const originalCommitment = new BigDecimal(userOutcome.totalBalance); 57 | const userTotalAmountTimesBeta = new BigDecimal(userOutcome.totalWeightedBalance); 58 | const profit = userTotalAmountTimesBeta.times(winnerProfits).div(winningOutcomeTotalAmountTimesBeta); 59 | const totalClaimAmount = originalCommitment.plus(profit); 60 | 61 | assert(userOutcome.userOutcomeTimeslots !== MISSING); 62 | 63 | const tokenIds = userOutcome.userOutcomeTimeslots.map(userOutcomeTimeslot => { 64 | assert(userOutcomeTimeslot.outcomeTimeslot !== MISSING); 65 | assert(userOutcomeTimeslot.outcomeTimeslot.tokenId !== MISSING); 66 | return EthersBigInteger.from(userOutcomeTimeslot.outcomeTimeslot.tokenId); 67 | }); 68 | 69 | return { 70 | claimType: VirtualFloorClaimType.Payouts, 71 | totalClaimAmount, 72 | tokenIds 73 | } 74 | } else /* if (vf.winningOutcome.userOutcomes.length === 0) */ { 75 | return { 76 | claimType: VirtualFloorClaimType.Payouts, 77 | totalClaimAmount: new BigDecimal(0), 78 | tokenIds: [] 79 | } 80 | } 81 | } 82 | case VirtualFloorEntityState.Claimable_Refunds_Flagged: 83 | case VirtualFloorEntityState.Claimable_Refunds_ResolvedNoWinners: 84 | case VirtualFloorEntityState.Claimable_Refunds_ResolvableNever: { 85 | assert(vf.outcomes); 86 | 87 | // ToDo: What we really want to assert here, is that all outcomes are included on the query response, 88 | // (e.g. the query has not filtered out one of the outcomes) 89 | // Ideally, we would generate bindings directly from named GQL queries, 90 | // and we would not need to encode these assertions manually. 91 | assert(vf.outcomes.length >= 2); 92 | 93 | const individualOutcomeSubClaims = vf.outcomes.map((outcome: OutcomeEntity): Omit => { 94 | assert(outcome.userOutcomes !== MISSING); 95 | assert(outcome.userOutcomes.length === 0 || outcome.userOutcomes.length === 1); 96 | if (outcome.userOutcomes.length === 1) { 97 | const [userOutcome] = outcome.userOutcomes; 98 | assert(userOutcome.totalBalance !== MISSING); 99 | const totalClaimAmount = new BigDecimal(userOutcome.totalBalance); 100 | assert(userOutcome.userOutcomeTimeslots !== MISSING); 101 | const tokenIds = userOutcome.userOutcomeTimeslots.map(userOutcomeTimeslot => { 102 | assert(userOutcomeTimeslot.outcomeTimeslot !== MISSING); 103 | assert(userOutcomeTimeslot.outcomeTimeslot.tokenId !== MISSING); 104 | return EthersBigInteger.from(userOutcomeTimeslot.outcomeTimeslot.tokenId); 105 | }); 106 | return { 107 | totalClaimAmount, 108 | tokenIds 109 | }; 110 | } else /* if (outcome.userOutcomes.length === 0) */ { 111 | return { 112 | totalClaimAmount: new BigDecimal(0), 113 | tokenIds: [] 114 | }; 115 | } 116 | }); 117 | 118 | return { 119 | claimType: VirtualFloorClaimType.Refunds, 120 | ...individualOutcomeSubClaims.reduce((aggregate, subClaim) => ({ 121 | totalClaimAmount: aggregate.totalClaimAmount.plus(subClaim.totalClaimAmount), 122 | tokenIds: [...aggregate.tokenIds, ...subClaim.tokenIds] 123 | })) 124 | }; 125 | } 126 | default: 127 | return null; 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /app/src/components/OutcomeComponent.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 174 | 175 | 184 | -------------------------------------------------------------------------------- /platform/gas-report.txt: -------------------------------------------------------------------------------- 1 | ·-----------------------------------------------------------------|---------------------------|-------------|-----------------------------· 2 | | Solc version: 0.8.12 · Optimizer enabled: true · Runs: 100 · Block limit: 30000000 gas │ 3 | ··································································|···························|·············|······························ 4 | | Methods │ 5 | ·······························|··································|·············|·············|·············|···············|·············· 6 | | Contract · Method · Min · Max · Avg · # calls · eur (avg) │ 7 | ·······························|··································|·············|·············|·············|···············|·············· 8 | | ChallengeableCreatorOracle · cancelVirtualFloorUnresolvable · - · - · 48184 · 2 · - │ 9 | ·······························|··································|·············|·············|·············|···············|·············· 10 | | ChallengeableCreatorOracle · challengeSetResult · - · - · 79034 · 2 · - │ 11 | ·······························|··································|·············|·············|·············|···············|·············· 12 | | ChallengeableCreatorOracle · claimPayouts · 59674 · 86940 · 80870 · 18 · - │ 13 | ·······························|··································|·············|·············|·············|···············|·············· 14 | | ChallengeableCreatorOracle · claimRefunds · 58623 · 75723 · 67173 · 4 · - │ 15 | ·······························|··································|·············|·············|·············|···············|·············· 16 | | ChallengeableCreatorOracle · commitToVirtualFloor · 91672 · 163430 · 134488 · 92 · - │ 17 | ·······························|··································|·············|·············|·············|···············|·············· 18 | | ChallengeableCreatorOracle · confirmUnchallengedResult · 88972 · 155628 · 124402 · 22 · - │ 19 | ·······························|··································|·············|·············|·············|···············|·············· 20 | | ChallengeableCreatorOracle · createVirtualFloor · 139532 · 144392 · 144232 · 98 · - │ 21 | ·······························|··································|·············|·············|·············|···············|·············· 22 | | ChallengeableCreatorOracle · finalizeChallenge · 96030 · 132299 · 120182 · 6 · - │ 23 | ·······························|··································|·············|·············|·············|···············|·············· 24 | | ChallengeableCreatorOracle · finalizeUnsetResult · - · - · 139554 · 2 · - │ 25 | ·······························|··································|·············|·············|·············|···············|·············· 26 | | ChallengeableCreatorOracle · safeTransferFrom · 66656 · 71593 · 69125 · 4 · - │ 27 | ·······························|··································|·············|·············|·············|···············|·············· 28 | | ChallengeableCreatorOracle · setResult · 66796 · 66856 · 66851 · 26 · - │ 29 | ·······························|··································|·············|·············|·············|···············|·············· 30 | | ChallengeableCreatorOracle · updatePaymentTokenWhitelist · - · - · 55470 · 12 · - │ 31 | ·······························|··································|·············|·············|·············|···············|·············· 32 | | CreationQuotas · adjustCreationQuotas · 40138 · 57250 · 53818 · 20 · - │ 33 | ·······························|··································|·············|·············|·············|···············|·············· 34 | | DummyUSDCoin · approve · 26354 · 46290 · 33664 · 112 · - │ 35 | ·······························|··································|·············|·············|·············|···············|·············· 36 | | DummyUSDCoin · mint · 38759 · 72995 · 46709 · 112 · - │ 37 | ·······························|··································|·············|·············|·············|···············|·············· 38 | | ERC20PresetMinterPauser · grantRole · - · - · 58760 · 12 · - │ 39 | ·······························|··································|·············|·············|·············|···············|·············· 40 | | Deployments · · % of limit · │ 41 | ··································································|·············|·············|·············|···············|·············· 42 | | DoubleDice · - · - · 5339072 · 17.8 % · - │ 43 | ··································································|·············|·············|·············|···············|·············· 44 | | DummyUSDCoin · - · - · 1750599 · 5.8 % · - │ 45 | ··································································|·············|·············|·············|···············|·············· 46 | | GraphHelper · - · - · 748268 · 2.5 % · - │ 47 | ·-----------------------------------------------------------------|-------------|-------------|-------------|---------------|-------------· -------------------------------------------------------------------------------- /platform/helpers/deployment.ts: -------------------------------------------------------------------------------- 1 | import { BytesLike } from 'ethers'; 2 | import { SignerWithAddress } from '.'; 3 | import { 4 | DoubleDice, 5 | DoubleDice__factory, 6 | DummyUSDCoin, 7 | DummyUSDCoin__factory, 8 | DummyWrappedBTC, 9 | DummyWrappedBTC__factory, 10 | GraphHelper, 11 | GraphHelper__factory, 12 | ProxyAdmin__factory, 13 | TransparentUpgradeableProxy__factory 14 | } from '../lib/contracts'; 15 | 16 | // Here we could simply use @openzeppelin/hardhat-upgrades deployProxy function, 17 | // but it does not work yet, 18 | // compilation fails with error "Error: No node with id 5102 of type StructDefinition,EnumDefinition" 19 | // Probably because user-defined value types are not yet supported: 20 | // https://github.com/OpenZeppelin/openzeppelin-upgrades/issues/477 21 | // This replacement can be dropped as soon as there is support 22 | 23 | export const deployProxy = async ({ 24 | name, 25 | proxyAdminAddress, 26 | deployer, 27 | deployedImplAddress, 28 | encodedInitializerData, 29 | }: { 30 | name: string; 31 | proxyAdminAddress?: string, 32 | deployer: SignerWithAddress; 33 | deployedImplAddress: string; 34 | encodedInitializerData: BytesLike; 35 | }): Promise => { 36 | if (proxyAdminAddress) { 37 | process.stdout.write(`Using ProxyAdmin already deployed at ${proxyAdminAddress}\n`); 38 | } else { 39 | const proxyAdmin = await new ProxyAdmin__factory(deployer).deploy(); 40 | process.stdout.write(`Deploying ProxyAdmin to: ${proxyAdmin.address}...\n`); 41 | process.stdout.write(`Sent transaction: ${proxyAdmin.deployTransaction.hash}\n`); 42 | await proxyAdmin.deployed(); 43 | proxyAdminAddress = proxyAdmin.address; 44 | } 45 | 46 | const proxy = await new TransparentUpgradeableProxy__factory(deployer).deploy( 47 | deployedImplAddress, 48 | proxyAdminAddress, 49 | encodedInitializerData 50 | ); 51 | 52 | process.stdout.write(`Deploying ${name} proxy to: ${proxy.address}...\n`); 53 | process.stdout.write(`Sent transaction: ${proxy.deployTransaction.hash}\n`); 54 | await proxy.deployed(); 55 | 56 | return proxy.address; 57 | }; 58 | 59 | export async function deployDummyUSDCoin(deployer: SignerWithAddress): Promise { 60 | const contract = await new DummyUSDCoin__factory(deployer).deploy(); 61 | process.stdout.write(`Deploying USDC contract to: ${contract.address}...\n`); 62 | process.stdout.write(`Sent transaction: ${contract.deployTransaction.hash}\n`); 63 | await contract.deployed(); 64 | return contract; 65 | } 66 | 67 | export async function deployDummyWrappedBTC(deployer: SignerWithAddress): Promise { 68 | const contract = await new DummyWrappedBTC__factory(deployer).deploy(); 69 | process.stdout.write(`Deploying WBTC contract to: ${contract.address}...\n`); 70 | process.stdout.write(`Sent transaction: ${contract.deployTransaction.hash}\n`); 71 | await contract.deployed(); 72 | return contract; 73 | } 74 | 75 | export async function deployDoubleDice({ 76 | deployer, 77 | deployArgs, 78 | initializeArgs 79 | }: { 80 | deployer: SignerWithAddress; 81 | deployArgs: Parameters; 82 | initializeArgs: [Parameters[0], Parameters[1]]; // No TypeScript magic can do this for now 83 | }): Promise { 84 | const impl = await new DoubleDice__factory(deployer).deploy(...deployArgs); 85 | process.stdout.write(`Deploying DoubleDice impl to: ${impl.address}...\n`); 86 | process.stdout.write(`Sent transaction: ${impl.deployTransaction.hash}\n`); 87 | await impl.deployed(); 88 | const encodedInitializerData = impl.interface.encodeFunctionData('initialize', initializeArgs); 89 | const proxyAddress = await deployProxy({ name: 'DoubleDice', deployer: deployer, deployedImplAddress: impl.address, encodedInitializerData }); 90 | const contract = DoubleDice__factory.connect(proxyAddress, deployer); 91 | 92 | process.stdout.write(`Granting OPERATOR_ROLE to admin ${deployer.address}\n`); 93 | await (await contract.grantRole(await contract.OPERATOR_ROLE(), deployer.address)).wait(); 94 | 95 | process.stdout.write(`Granting quota of 100 rooms to admin ${deployer.address}\n`); 96 | await (await contract.adjustCreationQuotas([{ creator: deployer.address, relativeAmount: 100 }])).wait(); 97 | 98 | return contract; 99 | } 100 | 101 | export async function deployGraphHelper({ 102 | deployer, 103 | proxyAdminAddress, 104 | }: { 105 | deployer: SignerWithAddress; 106 | proxyAdminAddress?: string; 107 | }): Promise { 108 | const graphHelperImpl = await new GraphHelper__factory(deployer).deploy(); 109 | process.stdout.write(`Deploying GraphHelper impl to: ${graphHelperImpl.address}...\n`); 110 | process.stdout.write(`Sent transaction: ${graphHelperImpl.deployTransaction.hash}\n`); 111 | await graphHelperImpl.deployed(); 112 | const proxyAddress = await deployProxy({ 113 | name: 'GraphHelper', 114 | proxyAdminAddress, 115 | deployer, 116 | deployedImplAddress: graphHelperImpl.address, 117 | encodedInitializerData: '0x', 118 | }); 119 | return GraphHelper__factory.connect(proxyAddress, deployer); 120 | } 121 | 122 | export async function upgradeDoubleDice({ 123 | deployer, 124 | deployArgs, 125 | deployedTransparentUpgradeableProxyAddress, 126 | deployedProxyAdminAddress, 127 | }: { 128 | deployer: SignerWithAddress; 129 | deployArgs: Parameters; 130 | deployedTransparentUpgradeableProxyAddress: string; 131 | deployedProxyAdminAddress: string; 132 | }): Promise { 133 | 134 | const impl = await new DoubleDice__factory(deployer).deploy(...deployArgs); 135 | process.stdout.write(`Deploying DoubleDice impl to: ${impl.address}...\n`); 136 | process.stdout.write(`Sent transaction: ${impl.deployTransaction.hash}\n`); 137 | await impl.deployed(); 138 | process.stdout.write('Deployed.\n\n'); 139 | const implAddress = impl.address; 140 | 141 | // Note: If impl is deployed correctly, but for some reason upgrade fails with 142 | // "execution reverted: ERC1967: new implementation is not a contract", 143 | // comment the code block above, set implAddress directly, and reattempt upgrade. 144 | 145 | process.stdout.write(`Calling ProxyAdmin(${deployedProxyAdminAddress}),\n`); 146 | process.stdout.write(` to upgrade TransparentUpgradeableProxyAddress(${deployedTransparentUpgradeableProxyAddress}),\n`); 147 | process.stdout.write(` to just-deployed impl DoubleDice(${implAddress})...\n`); 148 | const proxyAdmin = ProxyAdmin__factory.connect(deployedProxyAdminAddress, deployer); 149 | await (await proxyAdmin.upgrade(deployedTransparentUpgradeableProxyAddress, implAddress)).wait(); 150 | process.stdout.write('Upgraded.\n'); 151 | } 152 | -------------------------------------------------------------------------------- /platform/subgraph/schema.graphql: -------------------------------------------------------------------------------- 1 | # References: 2 | # - https://thegraph.com/docs/developer/assemblyscript-api 3 | # - https://thegraph.academy/developers/defining-a-subgraph/ 4 | # - https://dev.to/dabit3/building-graphql-apis-on-ethereum-4poa 5 | # - https://github.com/ensdomains/ens-subgraph 6 | # - https://github.com/graphprotocol/dharma-subgraph 7 | 8 | type Category @entity { 9 | id: ID! 10 | 11 | # children 12 | subcategories: [Subcategory!]! @derivedFrom(field: "category") 13 | 14 | # properties 15 | slug: String! 16 | } 17 | 18 | type Subcategory @entity { 19 | id: ID! 20 | 21 | # parents 22 | category: Category! 23 | 24 | # children 25 | virtualFloors: [VirtualFloor!]! @derivedFrom(field: "subcategory") 26 | 27 | # properties 28 | slug: String! 29 | } 30 | 31 | enum VirtualFloorState { 32 | Active_ResultNone, # formerly RUNNING_OR_CLOSED__RESULT_NONE 33 | Active_ResultSet, # formerly RUNNING_OR_CLOSED__RESULT_SET 34 | Active_ResultChallenged, # formerly RUNNING_OR_CLOSED__RESULT_CHALLENGED 35 | Claimable_Refunds_ResolvableNever, # formerly CANCELLED_BECAUSE_UNRESOLVABLE 36 | Claimable_Refunds_ResolvedNoWinners, # formerly CANCELLED_BECAUSE_RESOLVED_NO_WINNERS 37 | Claimable_Refunds_Flagged, # formerly CANCELLED_BECAUSE_FLAGGED 38 | Claimable_Payouts # formerly RESOLVED_WINNERS 39 | } 40 | 41 | type VirtualFloor @entity { 42 | id: ID! 43 | 44 | intId: BigInt! 45 | 46 | # parents 47 | owner: User! 48 | subcategory: Subcategory! 49 | 50 | """Only set if the result set by the creator has been challenged""" 51 | challenger: User 52 | 53 | # children 54 | outcomes: [Outcome!]! @derivedFrom(field: "virtualFloor") 55 | opponents: [Opponent!]! @derivedFrom(field: "virtualFloor") 56 | resultSources: [ResultSource!]! @derivedFrom(field: "virtualFloor") 57 | 58 | # properties (event) 59 | paymentToken: PaymentToken! 60 | betaOpen: BigDecimal! 61 | creationFeeRate: BigDecimal! 62 | platformFeeRate: BigDecimal! 63 | tCreated: BigInt! 64 | tOpen: BigInt! 65 | tClose: BigInt! 66 | tResolve: BigInt! 67 | bonusAmount: BigDecimal! 68 | minCommitmentAmount: BigDecimal! 69 | maxCommitmentAmount: BigDecimal! 70 | 71 | # Despite being redundant, as it is always equal to tResolve, 72 | # this could be different in a different Oracle implementation. 73 | # So we make a distinction between `tResolve` the VF creation parameter, 74 | # and `tResultSetMin` the value pertaining to the current particular Oracle implementation 75 | tResultSetMin: BigInt! 76 | 77 | # ToDo: Emit per-VF as VirtualFloorCreated event field 78 | # Optional because it can only be set once SET_WINDOW starts ticking 79 | tResultSetMax: BigInt! 80 | 81 | # ToDo: Emit per-VF as VirtualFloorCreated event field 82 | # Optional because it can only be set once CHALLENGE_WINDOW starts ticking 83 | tResultChallengeMax: BigInt 84 | 85 | state: VirtualFloorState! 86 | winningOutcome: Outcome # Optional: Only set if VF is resolved 87 | winnerProfits: BigDecimal # Optional: Only set if VF is resolved 88 | flaggingReason: String # Optional: Only set if VF is cancelled because it was flagged 89 | 90 | # properties (ipfs) 91 | title: String! 92 | description: String! 93 | isListed: Boolean! 94 | discordChannelId: String! 95 | 96 | # aggregates 97 | totalSupply: BigDecimal! 98 | } 99 | 100 | type Opponent @entity { 101 | id: ID! 102 | 103 | # parents 104 | virtualFloor: VirtualFloor! 105 | 106 | # properties 107 | title: String! 108 | image: String! 109 | } 110 | 111 | type ResultSource @entity { 112 | id: ID! 113 | 114 | # parents 115 | virtualFloor: VirtualFloor! 116 | 117 | # properties 118 | title: String! 119 | url: String! 120 | } 121 | 122 | type Outcome @entity { 123 | id: ID! 124 | 125 | # parents 126 | virtualFloor: VirtualFloor! 127 | 128 | # children 129 | outcomeTimeslots: [OutcomeTimeslot!]! @derivedFrom(field: "outcome") 130 | userOutcomeTimeslots: [UserOutcomeTimeslot!]! @derivedFrom(field: "outcome") 131 | userOutcomes: [UserOutcome!]! @derivedFrom(field: "outcome") 132 | 133 | # properties 134 | title: String! 135 | index: Int! 136 | 137 | # aggregates 138 | totalSupply: BigDecimal! 139 | totalWeightedSupply: BigDecimal! 140 | } 141 | 142 | type User @entity { 143 | id: ID! 144 | 145 | # children 146 | userOutcomeTimeslots: [UserOutcomeTimeslot!]! @derivedFrom(field: "user") 147 | userOutcomes: [UserOutcome!]! @derivedFrom(field: "user") 148 | outcomeTimeslotTransfersTo: [OutcomeTimeslotTransfer!]! @derivedFrom(field: "to") 149 | outcomeTimeslotTransfersFrom: [OutcomeTimeslotTransfer!]! @derivedFrom(field: "from") 150 | ownedVirtualFloors: [VirtualFloor!]! @derivedFrom(field: "owner") 151 | challengedVirtualFloors: [VirtualFloor!]! @derivedFrom(field: "challenger") 152 | 153 | # properties 154 | maxConcurrentVirtualFloors: BigInt! 155 | concurrentVirtualFloors: BigInt! 156 | } 157 | 158 | type UserOutcome @entity { 159 | id: ID! 160 | 161 | # parents 162 | user: User! 163 | outcome: Outcome! 164 | 165 | # children 166 | userOutcomeTimeslots: [UserOutcomeTimeslot!]! @derivedFrom(field: "userOutcome") 167 | 168 | # properties 169 | totalBalance: BigDecimal! 170 | totalWeightedBalance: BigDecimal! 171 | } 172 | 173 | type OutcomeTimeslot @entity { 174 | id: ID! 175 | 176 | tokenId: BigInt! 177 | 178 | # parents 179 | outcome: Outcome! 180 | timeslot: BigInt! 181 | 182 | # children 183 | userOutcomeTimeslots: [UserOutcomeTimeslot!]! @derivedFrom(field: "outcomeTimeslot") 184 | outcomeTimeslotTransfers: [OutcomeTimeslotTransfer!]! @derivedFrom(field: "outcomeTimeslot") 185 | 186 | # properties 187 | totalSupply: BigDecimal! 188 | beta: BigDecimal! 189 | } 190 | 191 | type UserOutcomeTimeslot @entity { 192 | id: ID! 193 | 194 | # parents 195 | user: User! 196 | outcome: Outcome! 197 | 198 | """Deprecated: Use `UserOutcomeTimeslot.outcomeTimeslot.timeslot` instead.""" 199 | timeslot: BigInt! 200 | 201 | outcomeTimeslot: OutcomeTimeslot! 202 | userOutcome: UserOutcome! 203 | 204 | # # children 205 | # outcomeTimeslotTransfersFrom: [OutcomeTimeslotTransfer!]! @derivedFrom(field: "fromUserOutcomeTimeslot") 206 | # outcomeTimeslotTransfersTo: [OutcomeTimeslotTransfer!]! @derivedFrom(field: "toUserOutcomeTimeslot") 207 | 208 | # properties 209 | balance: BigDecimal! 210 | } 211 | 212 | type OutcomeTimeslotTransfer @entity { 213 | id: ID! 214 | 215 | # parents 216 | outcomeTimeslot: OutcomeTimeslot! 217 | from: User! 218 | to: User! 219 | # fromUserOutcomeTimeslot: UserOutcomeTimeslot! 220 | # toUserOutcomeTimeslot: UserOutcomeTimeslot! 221 | 222 | # properties 223 | timestamp: BigInt! 224 | 225 | # """ 226 | # Position of the associated event log in the block. 227 | 228 | # All transfers could be sorted chronologically by ordering by `[timestamp, logIndex]` 229 | # """ 230 | # logIndex: Int! 231 | 232 | amount: BigDecimal! 233 | } 234 | 235 | type PaymentToken @entity { 236 | id: ID! 237 | 238 | # properties 239 | address: Bytes! 240 | name: String! 241 | symbol: String! 242 | decimals: Int! 243 | } 244 | 245 | type VirtualFloorsAggregate @entity { 246 | id: ID! 247 | 248 | """The total number of VFs ever created.""" 249 | totalVirtualFloorsCreated: Int! 250 | 251 | } 252 | -------------------------------------------------------------------------------- /platform/test/GraphHelper.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 2 | import chai, { expect } from 'chai'; 3 | import chaiSubset from 'chai-subset'; 4 | import { ethers } from 'hardhat'; 5 | import { 6 | deployGraphHelper 7 | } from '../helpers'; 8 | import { 9 | GraphHelper, 10 | GraphHelper__factory, 11 | VirtualFloorMetadataV1Struct 12 | } from '../lib/contracts'; 13 | 14 | chai.use(chaiSubset); 15 | 16 | describe('GraphHelper', () => { 17 | 18 | let deployer: SignerWithAddress; 19 | let graphHelper: GraphHelper; 20 | 21 | before(async function () { 22 | [deployer] = await ethers.getSigners(); 23 | graphHelper = await deployGraphHelper({ deployer }); 24 | }); 25 | 26 | it('decodeVirtualFloorMetadataV1', async () => { 27 | const orig: VirtualFloorMetadataV1Struct = { 28 | category: 'category', 29 | subcategory: 'subcategory', 30 | title: 'title', 31 | description: 'description', 32 | isListed: true, 33 | opponents: [ 34 | { title: 'opponents[0].title', image: 'opponents[0].image' }, 35 | { title: 'opponents[1].title', image: 'opponents[1].image' }, 36 | ], 37 | outcomes: [ 38 | { title: 'outcomes[0].title' }, 39 | { title: 'outcomes[1].title' }, 40 | { title: 'outcomes[2].title' }, 41 | ], 42 | resultSources: [ 43 | { title: 'resultSources[0].title', url: 'resultSources[0].url' } 44 | ], 45 | discordChannelId: 'discordChannelId', 46 | extraData: '0x1122334455', 47 | }; 48 | 49 | const encodedWithSelector = GraphHelper__factory.createInterface().encodeFunctionData('encodeVirtualFloorMetadataV1', [orig]) 50 | 51 | expect(encodedWithSelector).to.eq([ 52 | '0x92bedfc0', 53 | '0000000000000000000000000000000000000000000000000000000000000020', // 000: *decoded 54 | '0000000000000000000000000000000000000000000000000000000000000140', // 020: 000: *category 55 | '0000000000000000000000000000000000000000000000000000000000000180', // 020: *subcategory 56 | '00000000000000000000000000000000000000000000000000000000000001c0', // 040: *title 57 | '0000000000000000000000000000000000000000000000000000000000000200', // 060: *description 58 | '0000000000000000000000000000000000000000000000000000000000000001', // 080: isListed 59 | '0000000000000000000000000000000000000000000000000000000000000240', // 0a0: *opponents 60 | '0000000000000000000000000000000000000000000000000000000000000420', // 0c0: *outcomes 61 | '00000000000000000000000000000000000000000000000000000000000005c0', // 0f0: *resultSources 62 | '00000000000000000000000000000000000000000000000000000000000006c0', // 100: *discordChannelId 63 | '0000000000000000000000000000000000000000000000000000000000000700', // 120: *extraData 64 | '0000000000000000000000000000000000000000000000000000000000000008', // 140: category.length 65 | '63617465676f7279000000000000000000000000000000000000000000000000', // 160: "category" 66 | '000000000000000000000000000000000000000000000000000000000000000b', // 180: subcategory.length 67 | '73756263617465676f7279000000000000000000000000000000000000000000', // 1a0: "subcategory" 68 | '0000000000000000000000000000000000000000000000000000000000000005', // 1c0: title.length 69 | '7469746c65000000000000000000000000000000000000000000000000000000', // 1f0: "title" 70 | '000000000000000000000000000000000000000000000000000000000000000b', // 200: description.length 71 | '6465736372697074696f6e000000000000000000000000000000000000000000', // 220: "description" 72 | '0000000000000000000000000000000000000000000000000000000000000002', // 240: opponents.length 73 | '0000000000000000000000000000000000000000000000000000000000000040', // 260: 000: *opponents[0] 74 | '0000000000000000000000000000000000000000000000000000000000000100', // 280: 020: *opponents[1] 75 | '0000000000000000000000000000000000000000000000000000000000000040', // 2a0: 040: 000: *opponents[0].title 76 | '0000000000000000000000000000000000000000000000000000000000000080', // 2c0: 060: 020: *opponents[0].image 77 | '0000000000000000000000000000000000000000000000000000000000000012', // 2f0: 080: 040: opponents[0].title.length 78 | '6f70706f6e656e74735b305d2e7469746c650000000000000000000000000000', // 300: 0a0: 060: "opponents[0].title" 79 | '0000000000000000000000000000000000000000000000000000000000000012', // 320: 0c0: 080: opponents[0].image.length 80 | '6f70706f6e656e74735b305d2e696d6167650000000000000000000000000000', // 340: 100: 0a0: "opponents[0].image" 81 | '0000000000000000000000000000000000000000000000000000000000000040', // 360: ⋮ 82 | '0000000000000000000000000000000000000000000000000000000000000080', // 380: ⋮ 83 | '0000000000000000000000000000000000000000000000000000000000000012', // 3a0: ⋮ 84 | '6f70706f6e656e74735b315d2e7469746c650000000000000000000000000000', // 3c0: ⋮ 85 | '0000000000000000000000000000000000000000000000000000000000000012', // 3f0: ⋮ 86 | '6f70706f6e656e74735b315d2e696d6167650000000000000000000000000000', // 400: ⋮ 87 | '0000000000000000000000000000000000000000000000000000000000000003', // 420: outcomes.length 88 | '0000000000000000000000000000000000000000000000000000000000000060', // 440: ⋮ 89 | '00000000000000000000000000000000000000000000000000000000000000c0', // 460: ⋮ 90 | '0000000000000000000000000000000000000000000000000000000000000120', // 480: ⋮ 91 | '0000000000000000000000000000000000000000000000000000000000000020', // 4a0: ⋮ 92 | '0000000000000000000000000000000000000000000000000000000000000011', // 4c0: ⋮ 93 | '6f7574636f6d65735b305d2e7469746c65000000000000000000000000000000', // 4f0: ⋮ 94 | '0000000000000000000000000000000000000000000000000000000000000020', // 500: ⋮ 95 | '0000000000000000000000000000000000000000000000000000000000000011', // 520: ⋮ 96 | '6f7574636f6d65735b315d2e7469746c65000000000000000000000000000000', // 540: ⋮ 97 | '0000000000000000000000000000000000000000000000000000000000000020', // 560: ⋮ 98 | '0000000000000000000000000000000000000000000000000000000000000011', // 580: ⋮ 99 | '6f7574636f6d65735b325d2e7469746c65000000000000000000000000000000', // 5a0: ⋮ 100 | '0000000000000000000000000000000000000000000000000000000000000001', // 5c0: resultSources.length 101 | '0000000000000000000000000000000000000000000000000000000000000020', // 5f0: ⋮ 102 | '0000000000000000000000000000000000000000000000000000000000000040', // 600: ⋮ 103 | '0000000000000000000000000000000000000000000000000000000000000080', // 620: ⋮ 104 | '0000000000000000000000000000000000000000000000000000000000000016', // 640: ⋮ 105 | '726573756c74536f75726365735b305d2e7469746c6500000000000000000000', // 660: ⋮ 106 | '0000000000000000000000000000000000000000000000000000000000000014', // 680: ⋮ 107 | '726573756c74536f75726365735b305d2e75726c000000000000000000000000', // 6a0: ⋮ 108 | '0000000000000000000000000000000000000000000000000000000000000010', // 6c0: discordChannelId.length 109 | '646973636f72644368616e6e656c496400000000000000000000000000000000', // 6f0: "discordChannelId" 110 | '0000000000000000000000000000000000000000000000000000000000000005', // 700: extraData.length 111 | '1122334455000000000000000000000000000000000000000000000000000000', // 720: extraData 112 | ].join('')); 113 | const encoded = ethers.utils.hexDataSlice(encodedWithSelector, 4); 114 | expect(await graphHelper.encodeVirtualFloorMetadataV1(orig)).to.eq(encoded); 115 | const decoded = await graphHelper.decodeVirtualFloorMetadataV1(encoded); 116 | expect(decoded).to.containSubset(orig); 117 | }); 118 | 119 | }); 120 | -------------------------------------------------------------------------------- /platform/contracts/interface/IDoubleDice.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | pragma solidity 0.8.12; 3 | 4 | import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/IERC1155MetadataURIUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 7 | 8 | import "../library/FixedPointTypes.sol"; 9 | 10 | uint256 constant UNSPECIFIED_ZERO = 0; 11 | 12 | /// @notice The version defines how to interpret the data. 13 | /// In v1 the data could be abi-encoded, in v2 it could be JSON-encoded, 14 | /// and in v3 the data could be just a sha256 hash of the content. 15 | /// In v4 it could contain a server-signature. 16 | /// It doesn't matter. 17 | struct EncodedVirtualFloorMetadata { 18 | bytes32 version; 19 | bytes data; 20 | } 21 | 22 | struct VirtualFloorCreationParams { 23 | 24 | /// @notice Lower 5 bytes must be 0x00_00_00_00_00. Upper 27 bytes must be unique. 25 | /// Since all VF-related functions accept this id as an argument, 26 | /// it pays to choose an id with more zero-bytes, as these waste less intrinsic gas, 27 | /// and the savings will add up in the long run. 28 | /// Suggestion: This id could be of the form 0xVV_VV_VV_VV_00_00_00_00_00 29 | uint256 virtualFloorId; 30 | 31 | /// @notice Should be >= 1.0 32 | /// Should be scaled by 1e18 33 | UFixed256x18 betaOpen_e18; 34 | 35 | /// @notice Should be <= 1.0 36 | /// E.g. 2.5% is represented as 0.025, which is passed as 0_025000_000000_000000 37 | /// creationFee = creatorFee + platformFee 38 | /// ToDo: Name differently, because this is only charged if VF wins, not only if created 39 | UFixed256x18 creationFeeRate_e18; 40 | 41 | uint32 tOpen; 42 | uint32 tClose; 43 | uint32 tResolve; 44 | 45 | uint8 nOutcomes; 46 | IERC20Upgradeable paymentToken; 47 | 48 | uint256 bonusAmount; 49 | 50 | /// @notice Leave unspecified by passing value 0 51 | uint256 optionalMinCommitmentAmount; 52 | 53 | /// @notice Leave unspecified by passing value 0 54 | uint256 optionalMaxCommitmentAmount; 55 | 56 | EncodedVirtualFloorMetadata metadata; 57 | } 58 | 59 | struct CreatedVirtualFloorParams { 60 | UFixed256x18 betaOpen_e18; 61 | UFixed256x18 creationFeeRate_e18; 62 | UFixed256x18 platformFeeRate_e18; 63 | uint32 tOpen; 64 | uint32 tClose; 65 | uint32 tResolve; 66 | uint8 nOutcomes; 67 | IERC20Upgradeable paymentToken; 68 | uint256 bonusAmount; 69 | uint256 minCommitmentAmount; 70 | uint256 maxCommitmentAmount; 71 | address creator; 72 | } 73 | 74 | enum VirtualFloorState { 75 | None, 76 | Active_Open_MaybeResolvableNever, // formerly Running 77 | Active_Open_ResolvableLater, // formerly Running 78 | Active_Closed_ResolvableNever, // formerly ClosedUnresolvable 79 | Active_Closed_ResolvableLater, // formerly ClosedPreResolvable 80 | Active_Closed_ResolvableNow, // formerly ClosedResolvable 81 | Claimable_Payouts, // formerly ResolvedWinners 82 | Claimable_Refunds_ResolvedNoWinners, // formerly CancelledResolvedNoWinners 83 | Claimable_Refunds_ResolvableNever, // formerly CancelledUnresolvable 84 | Claimable_Refunds_Flagged // formerly CancelledFlagged 85 | } 86 | 87 | enum VirtualFloorResolutionType { 88 | NoWinners, 89 | Winners 90 | } 91 | 92 | error UnauthorizedMsgSender(); 93 | 94 | error WrongVirtualFloorState(VirtualFloorState actualState); 95 | 96 | error TooEarly(); 97 | 98 | error TooLate(); 99 | 100 | error DuplicateVirtualFloorId(); 101 | 102 | /// @notice platformFeeRate <= 1.0 not satisfied 103 | error PlatformFeeRateTooLarge(); 104 | 105 | /// @notice Trying to create a VF with a non-whitelisted ERC-20 payment-token 106 | error PaymentTokenNotWhitelisted(); 107 | 108 | /// @notice A VF id's lower 5 bytes must be 0x00_00_00_00_00 109 | error InvalidVirtualFloorId(); 110 | 111 | /// @notice betaOpen >= 1.0 not satisfied 112 | error BetaOpenTooSmall(); 113 | 114 | /// @notice creationFeeRate <= 1.0 not satisfied 115 | error CreationFeeRateTooLarge(); 116 | 117 | /// @notice VF timeline does not satisfy relation tOpen < tClose <= tResolve 118 | error InvalidTimeline(); 119 | 120 | /// @notice _MIN_POSSIBLE <= min <= max <= _MAX_POSSIBLE not satisfied 121 | error InvalidMinMaxCommitmentAmounts(); 122 | 123 | /// @notice nOutcomes >= 2 not satisfied 124 | error NotEnoughOutcomes(); 125 | 126 | /// @notice outcomeIndex < nOutcomes not satisfied 127 | error OutcomeIndexOutOfRange(); 128 | 129 | /// @notice minCommitmentAmount <= amount <= maxCommitmentAmount not satisfied 130 | error CommitmentAmountOutOfRange(); 131 | 132 | error CommitmentBalanceTransferWhilePaused(); 133 | 134 | error CommitmentBalanceTransferRejection(uint256 id, VirtualFloorState state); 135 | 136 | /// @notice One of the token ids passed to a claim does not correspond to the passed virtualFloorId 137 | error MismatchedVirtualFloorId(uint256 tokenId); 138 | 139 | error ResolveWhilePaused(); 140 | 141 | error CommitmentDeadlineExpired(); 142 | 143 | 144 | interface IDoubleDice is 145 | IAccessControlUpgradeable, 146 | IERC1155MetadataURIUpgradeable 147 | { 148 | event VirtualFloorCreation( 149 | uint256 indexed virtualFloorId, 150 | address indexed creator, 151 | UFixed256x18 betaOpen_e18, 152 | UFixed256x18 creationFeeRate_e18, 153 | UFixed256x18 platformFeeRate_e18, 154 | uint32 tOpen, 155 | uint32 tClose, 156 | uint32 tResolve, 157 | uint8 nOutcomes, 158 | IERC20Upgradeable paymentToken, 159 | uint256 bonusAmount, 160 | uint256 minCommitmentAmount, 161 | uint256 maxCommitmentAmount, 162 | EncodedVirtualFloorMetadata metadata 163 | ); 164 | 165 | event UserCommitment( 166 | uint256 indexed virtualFloorId, 167 | address indexed committer, 168 | uint8 outcomeIndex, 169 | uint256 timeslot, 170 | uint256 amount, 171 | UFixed256x18 beta_e18, 172 | uint256 tokenId 173 | ); 174 | 175 | event VirtualFloorCancellationFlagged( 176 | uint256 indexed virtualFloorId, 177 | string reason 178 | ); 179 | 180 | event VirtualFloorCancellationUnresolvable( 181 | uint256 indexed virtualFloorId 182 | ); 183 | 184 | event VirtualFloorResolution( 185 | uint256 indexed virtualFloorId, 186 | uint8 winningOutcomeIndex, 187 | VirtualFloorResolutionType resolutionType, 188 | uint256 winnerProfits, 189 | uint256 platformFeeAmount, 190 | uint256 creatorFeeAmount 191 | ); 192 | 193 | 194 | function createVirtualFloor(VirtualFloorCreationParams calldata params) external; 195 | 196 | function commitToVirtualFloor(uint256 virtualFloorId, uint8 outcomeIndex, uint256 amount, uint256 deadline) external; 197 | 198 | function cancelVirtualFloorFlagged(uint256 virtualFloorId, string calldata reason) external; 199 | 200 | function cancelVirtualFloorUnresolvable(uint256 virtualFloorId) external; 201 | 202 | 203 | function claimRefunds(uint256 vfId, uint256[] calldata tokenIds) external; 204 | 205 | function claimPayouts(uint256 vfId, uint256[] calldata tokenIds) external; 206 | 207 | 208 | function platformFeeRate_e18() external view returns (UFixed256x18); 209 | 210 | function platformFeeBeneficiary() external view returns (address); 211 | 212 | function getVirtualFloorCreator(uint256 virtualFloorId) external view returns (address); 213 | 214 | function getVirtualFloorParams(uint256 virtualFloorId) external view returns (CreatedVirtualFloorParams memory); 215 | 216 | function getVirtualFloorState(uint256 virtualFloorId) external view returns (VirtualFloorState); 217 | 218 | 219 | function isPaymentTokenWhitelisted(IERC20Upgradeable token) external view returns (bool); 220 | 221 | 222 | // ---------- Admin functions ---------- 223 | 224 | event PlatformFeeBeneficiaryUpdate(address platformFeeBeneficiary); 225 | 226 | function setPlatformFeeBeneficiary(address platformFeeBeneficiary) external; 227 | 228 | event PlatformFeeRateUpdate(UFixed256x18 platformFeeRate_e18); 229 | 230 | function setPlatformFeeRate_e18(UFixed256x18 platformFeeRate_e18) external; 231 | 232 | event ContractURIUpdate(string contractURI); 233 | 234 | function setContractURI(string memory contractURI) external; 235 | 236 | event PaymentTokenWhitelistUpdate(IERC20Upgradeable indexed token, bool whitelisted); 237 | 238 | function updatePaymentTokenWhitelist(IERC20Upgradeable token, bool isWhitelisted) external; 239 | } 240 | -------------------------------------------------------------------------------- /platform/test/unit-test/0-CreateVirtualFloor.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiSubset from 'chai-subset'; 3 | import { BigNumber } from 'ethers'; 4 | import { ethers } from 'hardhat'; 5 | import { 6 | deployDoubleDice, 7 | deployDummyUSDCoin, 8 | DUMMY_METADATA, 9 | ENCODED_DUMMY_METADATA, 10 | EvmCheckpoint, 11 | EvmHelper, 12 | findContractEventArgs, 13 | SignerWithAddress, 14 | toFp18, 15 | toTimestamp 16 | } from '../../helpers'; 17 | import { 18 | DoubleDice, 19 | DummyUSDCoin, 20 | DummyWrappedBTC, 21 | encodeVirtualFloorMetadata, 22 | VirtualFloorCreationParamsStruct 23 | } from '../../lib/contracts'; 24 | 25 | chai.use(chaiSubset); 26 | 27 | const creationFeeRate_e18 = 50000_000000_000000n; // 0.05 = 5% 28 | 29 | const MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE = 10 * 60; 30 | 31 | describe('DoubleDice/Create', function () { 32 | let ownerSigner: SignerWithAddress; 33 | let secondCreator: SignerWithAddress; 34 | let platformFeeBeneficiarySigner: SignerWithAddress; 35 | let contract: DoubleDice; 36 | let token: DummyUSDCoin | DummyWrappedBTC; 37 | let evm: EvmHelper; 38 | let checkpoint: EvmCheckpoint; 39 | const virtualFloorId = '0x0000000000000000000000000000000000000000000000000123450000000000'; 40 | const tOpen = toTimestamp('2032-01-01T00:00:00'); 41 | const tClose = toTimestamp('2032-01-01T12:00:00'); 42 | const tResolve = toTimestamp('2032-01-02T00:00:00'); 43 | const nOutcomes = 3; 44 | const betaOpen_e18 = BigNumber.from(10) 45 | .pow(18) 46 | .mul(13); // 1 unit per hour 47 | let vfParams: VirtualFloorCreationParamsStruct; 48 | 49 | before(async () => { 50 | evm = new EvmHelper(ethers.provider); 51 | 52 | [ 53 | ownerSigner, 54 | platformFeeBeneficiarySigner, 55 | secondCreator 56 | ] = await ethers.getSigners(); 57 | 58 | // Deploy USDC Token 59 | token = await deployDummyUSDCoin(ownerSigner); 60 | 61 | contract = await deployDoubleDice({ 62 | deployer: ownerSigner, 63 | deployArgs: [], 64 | initializeArgs: [ 65 | { 66 | tokenMetadataUriTemplate: 'http://localhost:8080/token/{id}', 67 | platformFeeRate_e18: toFp18(0.50), // 50% 68 | platformFeeBeneficiary: platformFeeBeneficiarySigner.address, 69 | contractURI: 'http://localhost:8080/contract-metadata.json' 70 | }, 71 | token.address, 72 | ] 73 | }); 74 | 75 | expect(await contract.platformFeeRate_e18()).to.eq(500000_000000_000000n); 76 | 77 | // Assert fee beneficiary 78 | expect(await contract.platformFeeBeneficiary()).to.eq(platformFeeBeneficiarySigner.address); 79 | 80 | { 81 | expect( 82 | await contract.isPaymentTokenWhitelisted(token.address) 83 | ).to.be.false; 84 | await ( 85 | await contract 86 | .connect(ownerSigner) 87 | .updatePaymentTokenWhitelist(token.address, true) 88 | ).wait(); 89 | expect( 90 | await contract.isPaymentTokenWhitelisted(token.address) 91 | ).to.be.true; 92 | } 93 | 94 | vfParams = { 95 | virtualFloorId, 96 | betaOpen_e18, 97 | creationFeeRate_e18, 98 | tOpen, 99 | tClose, 100 | tResolve, 101 | nOutcomes, 102 | paymentToken: token.address, 103 | bonusAmount: 0, 104 | optionalMinCommitmentAmount: 0, 105 | optionalMaxCommitmentAmount: 0, 106 | metadata: ENCODED_DUMMY_METADATA, 107 | }; 108 | 109 | checkpoint = await EvmCheckpoint.create(); 110 | }); 111 | 112 | describe('tOpen < tClose <= tResolve', () => { 113 | beforeEach(async () => { 114 | await evm.setNextBlockTimestamp(tOpen); 115 | }); 116 | describe('tOpen < tClose', () => { 117 | it('tClose < tOpen reverts', async () => { 118 | await expect(contract.createVirtualFloor({ ...vfParams, tClose: tOpen - 1 })).to.be.revertedWith('InvalidTimeline()'); 119 | }); 120 | it('tClose == tOpen reverts', async () => { 121 | await expect(contract.createVirtualFloor({ ...vfParams, tClose: tOpen })).to.be.revertedWith('InvalidTimeline()'); 122 | }); 123 | it('tClose > tOpen succeeds', async () => { 124 | await expect(contract.createVirtualFloor({ ...vfParams, tClose: tOpen + 1 })).to.emit(contract, 'VirtualFloorCreation'); 125 | }); 126 | }); 127 | describe('tClose <= tResolve', () => { 128 | it('tResolve < tClose reverts', async () => { 129 | await expect(contract.createVirtualFloor({ ...vfParams, tResolve: tClose - 1 })).to.be.revertedWith('InvalidTimeline()'); 130 | }); 131 | it('tResolve < tClose + MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE reverts', async () => { 132 | await expect(contract.createVirtualFloor({ ...vfParams, tResolve: tClose + MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE - 1 })).to.be.revertedWith('InvalidTimeline()'); 133 | }); 134 | it('tResolve == tClose + MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE succeeds', async () => { 135 | await expect(contract.createVirtualFloor({ ...vfParams, tResolve: tClose + MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE })).to.emit(contract, 'VirtualFloorCreation'); 136 | }); 137 | it('tResolve > tClose + MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE succeeds', async () => { 138 | await expect(contract.createVirtualFloor({ ...vfParams, tResolve: tClose + MIN_POSSIBLE_T_RESOLVE_MINUS_T_CLOSE + 1 })).to.emit(contract, 'VirtualFloorCreation'); 139 | }); 140 | }); 141 | }); 142 | 143 | it('nOutcomes > 2', async () => { 144 | await expect(contract.createVirtualFloor({ ...vfParams, nOutcomes: 1 })).to.be.revertedWith('NotEnoughOutcomes()'); 145 | await expect(contract.createVirtualFloor({ 146 | ...vfParams, 147 | nOutcomes: 2, 148 | metadata: encodeVirtualFloorMetadata({ 149 | ...DUMMY_METADATA, 150 | outcomes: DUMMY_METADATA.outcomes.slice(0, 2) 151 | }) 152 | })).to.emit(contract, 'VirtualFloorCreation'); 153 | }); 154 | 155 | it('betaOpen >= 1.0', async () => { 156 | await expect(contract.createVirtualFloor({ ...vfParams, betaOpen_e18: 999999_999999_999999n })).to.be.revertedWith('BetaOpenTooSmall()'); 157 | await expect(contract.createVirtualFloor({ ...vfParams, betaOpen_e18: 1_000000_000000_000000n })).to.emit(contract, 'VirtualFloorCreation'); 158 | }); 159 | 160 | it('creationFeeRate <= 1.0', async () => { 161 | await expect(contract.createVirtualFloor({ ...vfParams, creationFeeRate_e18: 1_000000_000000_000001n })).to.be.revertedWith('CreationFeeRateTooLarge()'); 162 | await expect(contract.createVirtualFloor({ ...vfParams, creationFeeRate_e18: 1_000000_000000_000000n })).to.emit(contract, 'VirtualFloorCreation'); 163 | }); 164 | 165 | it('Creation must happen up to 10% into the Running period', async () => { 166 | 167 | const params = { 168 | ...vfParams, 169 | tOpen: toTimestamp('2032-01-30T00:00:00'), 170 | tClose: toTimestamp('2032-01-30T10:00:00'), // 10 hours later 171 | tResolve: toTimestamp('2032-01-30T12:00:00') 172 | }; 173 | 174 | const localCheckpoint = await EvmCheckpoint.create(); 175 | 176 | const tCreateMax = toTimestamp('2032-01-30T01:00:00'); 177 | 178 | evm.setNextBlockTimestamp(tCreateMax + 1); 179 | await expect(contract.createVirtualFloor(params)).to.be.revertedWith('TooLate()'); 180 | await localCheckpoint.revertTo(); 181 | 182 | evm.setNextBlockTimestamp(tCreateMax); 183 | const { events } = await (await contract.createVirtualFloor(params)).wait(); 184 | const { virtualFloorId } = findContractEventArgs(events, 'VirtualFloorCreation'); 185 | expect(virtualFloorId).to.eq(params.virtualFloorId); 186 | await localCheckpoint.revertTo(); 187 | }); 188 | 189 | it('Should assign creator correctly', async () => { 190 | await (await contract.connect(ownerSigner).adjustCreationQuotas([{ creator: secondCreator.address, relativeAmount: 1 }])).wait(); 191 | await (await contract.connect(secondCreator).createVirtualFloor(vfParams)).wait(); 192 | expect(await contract.getVirtualFloorCreator(virtualFloorId)).to.eq(secondCreator.address); 193 | }); 194 | 195 | it('Should create VF if right arguments passed', async () => { 196 | const { events } = await (await contract.createVirtualFloor(vfParams)).wait(); 197 | const virtualFloorCreationEventArgs = findContractEventArgs(events, 'VirtualFloorCreation'); 198 | expect(virtualFloorCreationEventArgs.virtualFloorId).to.eq(virtualFloorId); 199 | }); 200 | 201 | it('Should revert if VF with same id created before', async () => { 202 | await (await contract.createVirtualFloor(vfParams)).wait(); 203 | await expect(contract.createVirtualFloor(vfParams)).to.be.revertedWith('DuplicateVirtualFloorId()'); 204 | }); 205 | 206 | afterEach(async () => { 207 | await checkpoint.revertTo(); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /platform/test/unit-test/4-FeeRelated.test.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import chaiSubset from 'chai-subset'; 3 | import { BigNumber, BigNumberish } from 'ethers'; 4 | import { ethers } from 'hardhat'; 5 | import { 6 | $, 7 | deployDoubleDice, 8 | deployDummyUSDCoin, 9 | DoubleDicePlatformHelper, 10 | ENCODED_DUMMY_METADATA, 11 | EvmCheckpoint, 12 | EvmHelper, 13 | generateRandomVirtualFloorId, 14 | SignerWithAddress, 15 | toFp18, 16 | toTimestamp, 17 | UNSPECIFIED_COMMITMENT_DEADLINE, 18 | UserCommitment 19 | } from '../../helpers'; 20 | import { 21 | DoubleDice, 22 | DummyUSDCoin, 23 | DummyWrappedBTC, 24 | VirtualFloorCreationParamsStruct 25 | } from '../../lib/contracts'; 26 | 27 | chai.use(chaiSubset); 28 | 29 | let helper: DoubleDicePlatformHelper; 30 | 31 | const creationFeeRate_e18 = 50000_000000_000000n; // 0.05 = 5% 32 | 33 | describe('DoubleDice/FeeRelated', function () { 34 | let ownerSigner: SignerWithAddress; 35 | let platformFeeBeneficiarySigner: SignerWithAddress; 36 | let user1Signer: SignerWithAddress; 37 | let user2Signer: SignerWithAddress; 38 | let user3Signer: SignerWithAddress; 39 | let user4Signer: SignerWithAddress; 40 | let contract: DoubleDice; 41 | let token: DummyUSDCoin | DummyWrappedBTC; 42 | let paymentTokenAddress: string; 43 | let evm: EvmHelper; 44 | 45 | before(async function () { 46 | evm = new EvmHelper(ethers.provider); 47 | 48 | [ 49 | ownerSigner, 50 | platformFeeBeneficiarySigner, 51 | user1Signer, 52 | user2Signer, 53 | user3Signer, 54 | user4Signer, 55 | ] = await ethers.getSigners(); 56 | 57 | // Deploy USDC Token 58 | token = await deployDummyUSDCoin(ownerSigner); 59 | 60 | contract = await deployDoubleDice({ 61 | deployer: ownerSigner, 62 | deployArgs: [], 63 | initializeArgs: [ 64 | { 65 | tokenMetadataUriTemplate: 'http://localhost:8080/token/{id}', 66 | platformFeeRate_e18: toFp18(0.50), // 50% 67 | platformFeeBeneficiary: platformFeeBeneficiarySigner.address, 68 | contractURI: 'http://localhost:8080/contract-metadata.json' 69 | }, 70 | token.address, 71 | ] 72 | }); 73 | 74 | expect(await contract.platformFeeRate_e18()).to.eq(500000_000000_000000n); 75 | 76 | helper = new DoubleDicePlatformHelper(contract); 77 | 78 | // Assert fee beneficiary 79 | expect(await contract.platformFeeBeneficiary()).to.eq(platformFeeBeneficiarySigner.address); 80 | 81 | { 82 | expect( 83 | await contract.isPaymentTokenWhitelisted(token.address) 84 | ).to.be.false; 85 | await ( 86 | await contract 87 | .connect(ownerSigner) 88 | .updatePaymentTokenWhitelist(token.address, true) 89 | ).wait(); 90 | expect( 91 | await contract.isPaymentTokenWhitelisted(token.address) 92 | ).to.be.true; 93 | paymentTokenAddress = token.address; 94 | } 95 | }); 96 | 97 | describe('Fee related tests', function () { 98 | let virtualFloorId: BigNumberish; 99 | const betaOpen_e18 = BigNumber.from(10) 100 | .pow(18) 101 | .mul(13); // 1 unit per hour 102 | const tOpen = toTimestamp('2022-06-01T10:00:00'); 103 | const tClose = toTimestamp('2032-01-01T10:00:00'); 104 | const tResolve = toTimestamp('2032-01-02T00:00:00'); 105 | const nOutcomes = 3; 106 | 107 | let virtualFloorCreationParams: VirtualFloorCreationParamsStruct; 108 | 109 | beforeEach(async () => { 110 | // Random virtual floor for each test case 111 | virtualFloorId = generateRandomVirtualFloorId(); 112 | virtualFloorCreationParams = { 113 | virtualFloorId, 114 | betaOpen_e18, 115 | creationFeeRate_e18, 116 | tOpen, 117 | tClose, 118 | tResolve, 119 | nOutcomes, 120 | paymentToken: paymentTokenAddress, 121 | bonusAmount: 0, 122 | optionalMinCommitmentAmount: 0, 123 | optionalMaxCommitmentAmount: 0, 124 | metadata: ENCODED_DUMMY_METADATA, 125 | }; 126 | await ( 127 | await contract.createVirtualFloor({ 128 | ...virtualFloorCreationParams, 129 | paymentToken: paymentTokenAddress, 130 | }) 131 | ).wait(); 132 | }); 133 | 134 | it('Users of equal commitment should get equal share', async () => { 135 | const checkpoint = await EvmCheckpoint.create(); 136 | const amountToCommit = $(100); 137 | 138 | await helper.mintTokenAndGiveAllowanceToContract({ 139 | mintAmount: amountToCommit, 140 | allowanceAmount: amountToCommit, 141 | contractAddress: contract.address, 142 | ownerSigner, 143 | usersSigner: [user1Signer, user2Signer, user3Signer], 144 | token 145 | }); 146 | 147 | // winners commitment 148 | const user1CommitmentEventArgs: UserCommitment = await helper.commitToVirtualFloor(virtualFloorId, 1, user1Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 149 | const user2CommitmentEventArgs: UserCommitment = await helper.commitToVirtualFloor(virtualFloorId, 1, user2Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 150 | 151 | // loser commitment 152 | await helper.commitToVirtualFloor(virtualFloorId, 0, user3Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 153 | 154 | await evm.setNextBlockTimestamp(tResolve); 155 | 156 | const [resolutionEvent] = await helper.setResultThenLaterConfirmUnchallengedResult(ownerSigner, virtualFloorId, 1); 157 | 158 | const user1BalanceBeforeClaim = await token.balanceOf(user1Signer.address); 159 | const user2BalanceBeforeClaim = await token.balanceOf(user1Signer.address); 160 | 161 | await helper.claimPayouts(user1Signer, virtualFloorId, [user1CommitmentEventArgs.tokenId]); 162 | await helper.claimPayouts(user2Signer, virtualFloorId, [user2CommitmentEventArgs.tokenId]); 163 | 164 | const user1BalanceAfterClaim = await token.balanceOf(user1Signer.address); 165 | const user2BalanceAfterClaim = await token.balanceOf(user1Signer.address); 166 | 167 | const collectedFee = await token.balanceOf(await contract.platformFeeBeneficiary()); 168 | 169 | const user1Profit = user1BalanceAfterClaim.sub(user1BalanceBeforeClaim); 170 | const user2Profit = user2BalanceAfterClaim.sub(user2BalanceBeforeClaim); 171 | 172 | expect(collectedFee).to.be.eq(resolutionEvent.platformFeeAmount); 173 | 174 | // Assert loser commitment is equal to winners profit + fee 175 | expect(amountToCommit).to.be.eq( 176 | resolutionEvent.winnerProfits 177 | .add(resolutionEvent.platformFeeAmount) 178 | .add(resolutionEvent.creatorFeeAmount) 179 | ); 180 | 181 | expect(user1Profit).to.be.gt(0); 182 | expect(user1Profit).to.be.eq(user2Profit); 183 | await checkpoint.revertTo(); 184 | }); 185 | 186 | 187 | it('Time span should affect only same amount after vf open time', async () => { 188 | const amountToCommit = $(100000000000); 189 | 190 | await helper.mintTokenAndGiveAllowanceToContract({ 191 | mintAmount: amountToCommit, 192 | allowanceAmount: amountToCommit, 193 | contractAddress: contract.address, 194 | ownerSigner, 195 | usersSigner: [user1Signer, user2Signer, user3Signer, user4Signer], 196 | token 197 | }); 198 | 199 | 200 | // set to open time 201 | await evm.setNextBlockTimestamp('2022-06-01T10:00:00'); 202 | // winners commitment 203 | const user1CommitmentEventArgs: UserCommitment = await helper.commitToVirtualFloor(virtualFloorId, 1, user1Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 204 | await evm.setNextBlockTimestamp('2028-06-01T10:00:00'); 205 | const user2CommitmentEventArgs: UserCommitment = await helper.commitToVirtualFloor(virtualFloorId, 1, user2Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 206 | await evm.setNextBlockTimestamp('2029-06-01T10:00:00'); 207 | const user3CommitmentEventArgs: UserCommitment = await helper.commitToVirtualFloor(virtualFloorId, 1, user3Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 208 | 209 | console.log('user1 commitment', user1CommitmentEventArgs.timeslot.toNumber()); 210 | console.log('user2 commitment', user2CommitmentEventArgs.timeslot.toNumber()); 211 | console.log('user3 commitment', user3CommitmentEventArgs.timeslot.toNumber()); 212 | 213 | console.log('beta commitment', user1CommitmentEventArgs.beta_e18); 214 | console.log('beta commitment', user3CommitmentEventArgs.beta_e18); 215 | 216 | // loser commitment 217 | await helper.commitToVirtualFloor(virtualFloorId, 0, user4Signer, amountToCommit, UNSPECIFIED_COMMITMENT_DEADLINE); 218 | 219 | 220 | expect(user3CommitmentEventArgs.tokenId).to.not.be.eq(user1CommitmentEventArgs.tokenId); 221 | expect(user3CommitmentEventArgs.beta_e18).to.not.be.eq(user1CommitmentEventArgs.beta_e18); 222 | 223 | await evm.setNextBlockTimestamp('2032-01-02T00:00:00'); 224 | const [resolutionEvent] = await helper.setResultThenLaterConfirmUnchallengedResult(ownerSigner, virtualFloorId, 1); 225 | 226 | const user1BalanceBeforeClaim = await token.balanceOf(user1Signer.address); 227 | const user2BalanceBeforeClaim = await token.balanceOf(user1Signer.address); 228 | 229 | await helper.claimPayouts(user1Signer, virtualFloorId, [user1CommitmentEventArgs.tokenId]); 230 | await helper.claimPayouts(user2Signer, virtualFloorId, [user2CommitmentEventArgs.tokenId]); 231 | await helper.claimPayouts(user3Signer, virtualFloorId, [user3CommitmentEventArgs.tokenId]); 232 | 233 | const user1BalanceAfterClaim = await token.balanceOf(user1Signer.address); 234 | const user2BalanceAfterClaim = await token.balanceOf(user1Signer.address); 235 | 236 | const collectedFee = await token.balanceOf(await contract.platformFeeBeneficiary()); 237 | 238 | const user1Profit = user1BalanceAfterClaim.sub(user1BalanceBeforeClaim); 239 | console.log('user1Profit', user1Profit); 240 | const user2Profit = user2BalanceAfterClaim.sub(user2BalanceBeforeClaim); 241 | console.log('user2Profit', user2Profit); 242 | 243 | expect(collectedFee).to.be.eq(resolutionEvent.platformFeeAmount); 244 | 245 | // Assert loser commitment is equal to winners profit + fee 246 | expect(amountToCommit).to.be.eq( 247 | resolutionEvent.winnerProfits 248 | .add(resolutionEvent.platformFeeAmount) 249 | .add(resolutionEvent.creatorFeeAmount) 250 | ); 251 | 252 | expect(user1Profit).to.be.gt(0); 253 | expect(user1Profit).to.be.eq(user2Profit); 254 | 255 | }); 256 | 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 20 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | /* Modules */ 24 | "module": "commonjs", /* Specify what module code is generated. */ 25 | // "rootDir": "./", /* Specify the root folder within your source files. */ 26 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 27 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 28 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 29 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 30 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 31 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 32 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 33 | // "resolveJsonModule": true, /* Enable importing .json files */ 34 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 35 | /* JavaScript Support */ 36 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 37 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 38 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 39 | /* Emit */ 40 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 41 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 42 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 43 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 44 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 45 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 46 | // "removeComments": true, /* Disable emitting comments. */ 47 | // "noEmit": true, /* Disable emitting files from a compilation. */ 48 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 49 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 50 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 51 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 54 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 55 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 56 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 57 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 58 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 59 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 60 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 61 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 62 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 63 | /* Interop Constraints */ 64 | "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 65 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 66 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 67 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 68 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 69 | /* Type Checking */ 70 | "strict": true, /* Enable all strict type-checking options. */ 71 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 72 | // See https://ajv.js.org/guide/typescript.html#utility-types-for-schemas 73 | "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 74 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 75 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 76 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 77 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 78 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 79 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 80 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 81 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 82 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 83 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 84 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 85 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 86 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 87 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 88 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 89 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 90 | /* Completeness */ 91 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 92 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 93 | } 94 | } --------------------------------------------------------------------------------