├── .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 |
2 |
18 |
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 |
2 |
3 | By category
4 |
5 | -
6 |
7 |
{{ category.slug }}
8 |
9 | -
10 |
11 |
{{ subcategory.slug }}
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
58 |
59 |
67 |
--------------------------------------------------------------------------------
/app/src/components/Timeline.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
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 |
2 |
29 |
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 |
2 |
32 |
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 |
34 |
--------------------------------------------------------------------------------
/app/src/assets/doubledice-usdc-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
2 |
3 |
4 |
5 | | {{ outcome.title }} |
6 |
7 |
8 | | total |
9 | {{ outcomeTotalSupply }} {{ virtualFloor.paymentToken.symbol }} |
10 | × |
11 | {{ outcomeAverageBeta.toFixed(2) }} |
12 |
13 |
14 | | user |
15 | {{ userTotalBalance }} {{ virtualFloor.paymentToken.symbol }} |
16 | × |
17 | {{ userAverageBeta.toFixed(2) }} |
18 |
19 |
20 | |
21 |
26 | |
27 |
28 |
29 | |
30 | {{ winningText }}
34 |
40 | |
41 |
42 |
43 | |
44 |
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 | }
--------------------------------------------------------------------------------