├── .github └── workflows │ ├── deploy-admin-state.yml │ ├── deploy.yml │ ├── docker-reward-calculator.yml │ ├── docker.yml │ ├── monitors-docker.yml │ ├── reclaim-docker.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── Readme.md ├── package.json ├── packages ├── balance-monitor │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── contract-admin-state │ ├── .env.example │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── assets │ │ └── logo.png │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── prettier.config.js │ ├── src │ │ ├── App.tsx │ │ ├── config │ │ │ └── contracts.ts │ │ ├── hooks │ │ │ └── useMulticall.ts │ │ ├── index.css │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── formatToken.ts │ │ │ └── toNumber.ts │ │ ├── vite-env.d.ts │ │ └── wagmi.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── contracts │ ├── .dockerignore │ ├── .gitignore │ ├── DEPLOY_TO_MAINNET.md │ ├── Dockerfile │ ├── Dockerfile-playground │ ├── Dockerfile-reclaimFunds │ ├── Dockerfile-registerGateway │ ├── Dockerfile.common │ ├── README.md │ ├── b58.py │ ├── deploy │ │ └── hardhat │ │ │ ├── 00_SQD.js │ │ │ ├── 01_WorkerRegistration.js │ │ │ ├── 02_RewardCalculation.js │ │ │ └── 03_RewardsDistribution.js │ ├── deployments │ │ ├── 42161.json │ │ ├── 421613.json │ │ ├── 421614.json │ │ └── arbitrum-goerli │ │ │ ├── .chainId │ │ │ ├── RewardCalculation.json │ │ │ ├── RewardTreasury.json │ │ │ ├── RewardsDistribution.json │ │ │ ├── WorkerRegistration.json │ │ │ ├── WorkerRegistrationFacade.json │ │ │ ├── solcInputs │ │ │ └── 4748128410615c77f58b8d981c2a2cb5.json │ │ │ └── tSQD.json │ ├── docker-compose.yaml │ ├── foundry.toml │ ├── package.json │ ├── register-gateway.sh │ ├── script │ │ ├── Bounty.s.sol │ │ ├── CreateVestings.s.sol │ │ ├── Deploy.s.sol │ │ ├── DeployDistributor.s.sol │ │ ├── PreparePlayground.s.sol │ │ ├── RedeployGateways.s.sol │ │ ├── RegisterGateway.s.sol │ │ ├── RegisterWorker.s.sol │ │ ├── UnlockFunds.s.sol │ │ ├── vestings.json │ │ └── workersSurvey.json │ ├── scripts │ │ ├── createMerkleTree.ts │ │ ├── fordefi │ │ │ ├── request.ts │ │ │ └── sendTransaction.ts │ │ └── sendVaultTokens.ts │ ├── spinup-testnet.sh │ ├── src │ │ ├── AccessControlledPausable.sol │ │ ├── AllocationsViewer.sol │ │ ├── DistributedRewardDistribution.sol │ │ ├── Executable.sol │ │ ├── GatewayRegistry.sol │ │ ├── LinearToSqrtCap.sol │ │ ├── MerkleDistributor.sol │ │ ├── NetworkController.sol │ │ ├── RewardCalculation.sol │ │ ├── RewardTreasury.sol │ │ ├── Router.sol │ │ ├── SQD.sol │ │ ├── SoftCap.sol │ │ ├── Staking.sol │ │ ├── TemporaryHolding.sol │ │ ├── TemporaryHoldingFactory.sol │ │ ├── Vesting.sol │ │ ├── VestingFactory.sol │ │ ├── WorkerRegistration.sol │ │ ├── arbitrum │ │ │ └── SQD.sol │ │ ├── gateway-strategies │ │ │ ├── EqualStrategy.sol │ │ │ └── SubequalStrategy.sol │ │ └── interfaces │ │ │ ├── IERC20WithMetadata.sol │ │ │ ├── IGatewayRegistry.sol │ │ │ ├── IGatewayStrategy.sol │ │ │ ├── INetworkController.sol │ │ │ ├── IRewardCalculation.sol │ │ │ ├── IRewardsDistribution.sol │ │ │ ├── IRouter.sol │ │ │ ├── IStaking.sol │ │ │ └── IWorkerRegistration.sol │ ├── test │ │ ├── BaseTest.sol │ │ ├── DistributedRewardsDistribution │ │ │ ├── DistributedRewardsDistribution.addAndRemoveDistributors.t.sol │ │ │ ├── DistributedRewardsDistribution.claim.t.sol │ │ │ ├── DistributedRewardsDistribution.commitAndApprove.t.sol │ │ │ ├── DistributedRewardsDistribution.distribute.t.sol │ │ │ └── DistributedRewardsDistribution.sol │ │ ├── GatewayRegistry │ │ │ ├── GatewayRegistry.allocate.t.sol │ │ │ ├── GatewayRegistry.allocatedCUs.t.sol │ │ │ ├── GatewayRegistry.autoextend.t.sol │ │ │ ├── GatewayRegistry.clusters.t.sol │ │ │ ├── GatewayRegistry.getActiveGateways.t.sol │ │ │ ├── GatewayRegistry.registrAndUnregister.t.sol │ │ │ ├── GatewayRegistry.stake.t.sol │ │ │ ├── GatewayRegistry.unstake.t.sol │ │ │ └── GatewayRegistryTest.sol │ │ ├── LinearToSqerCap.t.sol │ │ ├── NetworkController.t.sol │ │ ├── RewardCalculation.t.sol │ │ ├── RewardTreasury.t.sol │ │ ├── SoftCap.t.sol │ │ ├── Staking │ │ │ ├── StakersRewardDistributor.accessControl.t.sol │ │ │ ├── StakersRewardDistributor.deposit.t.sol │ │ │ ├── StakersRewardDistributor.withdraw.t.sol │ │ │ └── StakersRewardDistributorTest.sol │ │ ├── Vesting │ │ │ ├── SubsquidVesting.t.sol │ │ │ └── vesting_schedules.json │ │ ├── WorkerRegistration │ │ │ ├── WorkerRegistration.constructor.t.sol │ │ │ ├── WorkerRegistration.deregister.t.sol │ │ │ ├── WorkerRegistration.excessiveBond.t.sol │ │ │ ├── WorkerRegistration.register.t.sol │ │ │ ├── WorkerRegistration.sol │ │ │ ├── WorkerRegistration.updateMetadata.t.sol │ │ │ └── WorkerRegistration.withdraw.t.sol │ │ └── strategies │ │ │ └── EqualStrategy.t.sol │ └── tsconfig.json ├── reward-stats │ ├── .env.example │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── assets │ │ └── logo.png │ ├── index.html │ ├── package.json │ ├── polyfills.ts │ ├── postcss.config.js │ ├── prettier.config.js │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── RewardLinks.tsx │ │ │ ├── RewardsChart.tsx │ │ │ └── Stats.tsx │ │ ├── config │ │ │ └── contracts.ts │ │ ├── hooks │ │ │ ├── useBlockTimestamp.ts │ │ │ ├── useBond.ts │ │ │ ├── useRewards.ts │ │ │ ├── useStakes.ts │ │ │ └── useWorkers.ts │ │ ├── index.css │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── allWorkerIds.ts │ │ │ ├── formatToken.ts │ │ │ ├── stringify.ts │ │ │ └── toNumber.tsx │ │ ├── vite-env.d.ts │ │ └── wagmi.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts └── rewards-calculator │ ├── .dockerignore │ ├── .gitignore │ ├── .mocharc.json │ ├── Dockerfile │ ├── Dockerfile-endpoints │ ├── Readme.md │ ├── package.json │ ├── src │ ├── chain.ts │ ├── clickhouseClient.ts │ ├── config.ts │ ├── endpoints.ts │ ├── epochStats.ts │ ├── fordefi │ │ ├── getAddress.ts │ │ ├── request.ts │ │ └── sendTransaction.ts │ ├── index.ts │ ├── logger.ts │ ├── protobuf │ │ └── query.proto │ ├── reward.ts │ ├── rewardBot.ts │ ├── signatureVerification.ts │ ├── startBot.ts │ ├── testRPC.ts │ ├── utils.ts │ ├── worker.ts │ └── workers.ts │ ├── test │ ├── data │ │ └── test_log.json │ └── signature-verification.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.github/workflows/deploy-admin-state.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy contract admin state to Netlify 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | env: 13 | FOUNDRY_PROFILE: ci 14 | 15 | 16 | jobs: 17 | # Single deploy job since we're just deploying 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v2.0.1 28 | with: 29 | version: 9.0.3 30 | 31 | - name: Install deps 32 | run: pnpm i 33 | 34 | - name: Install Foundry 35 | uses: foundry-rs/foundry-toolchain@v1 36 | with: 37 | version: nightly 38 | - name: Run Build 39 | run: | 40 | pnpm build 41 | id: build 42 | - name: Deploy to Netlify 43 | uses: nwtgck/actions-netlify@v2.0 44 | with: 45 | publish-dir: './packages/contract-admin-state/dist' 46 | production-branch: main 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | deploy-message: "Deploy from GitHub Actions" 49 | enable-pull-request-comment: false 50 | enable-commit-comment: true 51 | overwrites-pull-request-comment: true 52 | env: 53 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 54 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_ADMIN_SITE_ID }} 55 | timeout-minutes: 1 56 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy stats to Netlify 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | env: 13 | FOUNDRY_PROFILE: ci 14 | 15 | 16 | jobs: 17 | # Single deploy job since we're just deploying 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v2.0.1 28 | with: 29 | version: 9.0.3 30 | 31 | - name: Install deps 32 | run: pnpm i 33 | 34 | - name: Install Foundry 35 | uses: foundry-rs/foundry-toolchain@v1 36 | with: 37 | version: nightly 38 | - name: Run Build 39 | run: | 40 | pnpm build 41 | id: build 42 | - name: Deploy to Netlify 43 | uses: nwtgck/actions-netlify@v2.0 44 | with: 45 | publish-dir: './packages/reward-stats/dist' 46 | production-branch: main 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | deploy-message: "Deploy from GitHub Actions" 49 | enable-pull-request-comment: false 50 | enable-commit-comment: true 51 | overwrites-pull-request-comment: true 52 | env: 53 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 54 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 55 | timeout-minutes: 1 56 | -------------------------------------------------------------------------------- /.github/workflows/docker-reward-calculator.yml: -------------------------------------------------------------------------------- 1 | name: docker-rewards-calculator 2 | on: 3 | workflow_dispatch: # manually run 4 | inputs: 5 | tag: 6 | description: image tag 7 | required: true 8 | 9 | env: 10 | CI: true 11 | 12 | jobs: 13 | publish: 14 | name: Build & publish docker image 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Docker login 21 | uses: docker/login-action@v1 22 | with: 23 | username: ${{ secrets.DOCKER_LOGIN }} 24 | password: ${{ secrets.DOCKER_TOKEN }} 25 | 26 | - name: Build & publish calculator image 27 | uses: docker/build-push-action@v3 28 | with: 29 | push: true 30 | context: . 31 | file: packages/rewards-calculator/Dockerfile 32 | tags: subsquid/rewards-calculator:${{ inputs.tag }} 33 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | workflow_dispatch: # manually run 4 | inputs: 5 | tag: 6 | description: image tag 7 | required: true 8 | 9 | env: 10 | CI: true 11 | 12 | jobs: 13 | publish: 14 | name: Build & publish docker image 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - run: cd packages/contracts 25 | 26 | - name: Docker login 27 | uses: docker/login-action@v1 28 | with: 29 | username: ${{ secrets.DOCKER_LOGIN }} 30 | password: ${{ secrets.DOCKER_TOKEN }} 31 | 32 | - name: Build & publish playground image 33 | uses: docker/build-push-action@v5 34 | with: 35 | push: true 36 | context: packages/contracts 37 | file: packages/contracts/Dockerfile-playground 38 | tags: subsquid/playground:${{ inputs.tag }} 39 | - name: Build & publish contracts image 40 | uses: docker/build-push-action@v5 41 | with: 42 | push: true 43 | context: packages/contracts 44 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,windows/amd64 45 | file: packages/contracts/Dockerfile-registerGateway 46 | tags: subsquid/register-gateway:latest 47 | -------------------------------------------------------------------------------- /.github/workflows/monitors-docker.yml: -------------------------------------------------------------------------------- 1 | name: Build balance monitor 2 | on: 3 | workflow_dispatch: # manually run 4 | inputs: 5 | tag: 6 | description: image tag 7 | required: true 8 | 9 | env: 10 | CI: true 11 | 12 | jobs: 13 | publish: 14 | name: Build & publish docker image 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - run: cd packages/contracts 25 | 26 | - name: Docker login 27 | uses: docker/login-action@v1 28 | with: 29 | username: ${{ secrets.DOCKER_LOGIN }} 30 | password: ${{ secrets.DOCKER_TOKEN }} 31 | 32 | - name: Build & publish balance monitor image 33 | uses: docker/build-push-action@v5 34 | with: 35 | push: true 36 | context: packages/balance-monitor 37 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 38 | tags: subsquid/balance-monitor:${{ inputs.tag }} 39 | 40 | - name: Build & publish rewards monitor image 41 | uses: docker/build-push-action@v5 42 | with: 43 | push: true 44 | context: . 45 | file: packages/rewards-calculator/Dockerfile-endpoints 46 | platforms: linux/amd64 47 | tags: subsquid/reward-monitor:${{ inputs.tag }} 48 | -------------------------------------------------------------------------------- /.github/workflows/reclaim-docker.yml: -------------------------------------------------------------------------------- 1 | name: Build reclaim image 2 | on: 3 | workflow_dispatch: # manually run 4 | inputs: 5 | tag: 6 | description: image tag 7 | required: true 8 | 9 | env: 10 | CI: true 11 | 12 | jobs: 13 | publish: 14 | name: Build & publish docker image 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | 24 | - run: cd packages/contracts 25 | 26 | - name: Docker login 27 | uses: docker/login-action@v1 28 | with: 29 | username: ${{ secrets.DOCKER_LOGIN }} 30 | password: ${{ secrets.DOCKER_TOKEN }} 31 | 32 | - name: Build & publish contracts image 33 | uses: docker/build-push-action@v5 34 | with: 35 | push: true 36 | context: packages/contracts 37 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,windows/amd64 38 | file: packages/contracts/Dockerfile-reclaimFunds 39 | tags: subsquid/reclaim:latest 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | FOUNDRY_PROFILE: ci 11 | 12 | jobs: 13 | check: 14 | strategy: 15 | fail-fast: true 16 | 17 | name: CI 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: recursive 23 | 24 | - name: Setup pnpm 25 | uses: pnpm/action-setup@v2.0.1 26 | with: 27 | version: 9.0.3 28 | 29 | - name: Install deps 30 | run: pnpm i 31 | 32 | - name: Install Foundry 33 | uses: foundry-rs/foundry-toolchain@v1 34 | with: 35 | version: nightly 36 | 37 | - name: Run Forge build 38 | run: | 39 | pnpm build 40 | id: build 41 | 42 | - name: Run Lint 43 | run: | 44 | pnpm lint 45 | id: lint 46 | 47 | - name: Run Forge tests 48 | run: | 49 | pnpm test 50 | id: test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .env 4 | .turbo 5 | .lockb -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "packages/contracts/lib/forge-std"] 2 | path = packages/contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "packages/contracts/lib/openzeppelin-contracts"] 5 | path = packages/contracts/lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | [submodule "packages/contracts/lib/prb-math"] 8 | path = packages/contracts/lib/prb-math 9 | url = https://github.com/PaulRBerg/prb-math 10 | [submodule "packages/contracts/lib/openzeppelin-contracts-upgradeable"] 11 | path = packages/contracts/lib/openzeppelin-contracts-upgradeable 12 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 13 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Subsquid Network Contracts 2 | 3 |

4 | 5 |

6 | 7 | This is a monorepo that contains contracts and utils that enable [Subsquid](https://subsquid.io/) decentralised network 8 | 9 | Subsquid uses [pnpm](https://pnpm.io/) as a package and monorepo manager. 10 | To install `pnpm`, run `npm install -g pnpm` or consult with [pnpm installation guide](https://pnpm.io/installation). 11 | 12 | Install all dependencies using 13 | ```bash 14 | pnpm install 15 | ``` 16 | 17 | ### Packages: 18 | - [Subsquid Network Contracts](./packages/contracts) 19 | - [Reward Simulator](./packages/rewards-calculator), process that calculates rewards based on 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "turbo build", 4 | "lint": "turbo lint", 5 | "test": "turbo test", 6 | "build-reward-image": "docker build -f packages/rewards-calculator/Dockerfile -t rewards-simulation ." 7 | }, 8 | "devDependencies": { 9 | "turbo": "^1.9.9" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/balance-monitor/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | README.md 4 | Dockerfile 5 | -------------------------------------------------------------------------------- /packages/balance-monitor/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | cache 3 | node_modules 4 | .idea 5 | pings.csv 6 | queries.csv 7 | stakes.csv 8 | *.pem 9 | -------------------------------------------------------------------------------- /packages/balance-monitor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | COPY package.json ./ 4 | RUN yarn 5 | 6 | COPY . . 7 | 8 | RUN yarn build 9 | EXPOSE 3000 10 | CMD ["node", "dist/index.js"] 11 | 12 | -------------------------------------------------------------------------------- /packages/balance-monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@subsquid-network/balance-monitor", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "engines": { 6 | "node": ">=16" 7 | }, 8 | "dependencies": { 9 | "express": "^4.19.2", 10 | "prom-client": "^15.1.2", 11 | "viem": "^2.9.31" 12 | }, 13 | "scripts": { 14 | "start": "TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"--loader ts-node/esm\" ts-node src/index.ts", 15 | "build": "tsc --outDir dist" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "^4.17.21", 19 | "@types/node": "^20.12.8", 20 | "typescript": "^5.4.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/balance-monitor/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, formatEther, http, parseAbi } from "viem"; 2 | import { arbitrum, arbitrumSepolia } from "viem/chains"; 3 | import express from "express"; 4 | import promClient from "prom-client"; 5 | 6 | if (!process.env.RPC_URL) { 7 | throw new Error("RPC_URL is not set"); 8 | } 9 | 10 | const ethBalanceWallets = (process.env.ETH_HOLDERS ?? "") 11 | .split(",") 12 | .map((address) => address.trim()) 13 | .filter((address) => address); 14 | 15 | const sqdBalanceWallets = (process.env.SQD_HOLDERS ?? "") 16 | .split(",") 17 | .map((address) => address.trim()) 18 | .filter((address) => address); 19 | 20 | if (ethBalanceWallets.length + sqdBalanceWallets.length === 0) { 21 | throw new Error("No wallets to monitor. Set ETH_HOLDERS and SQD_HOLDERS"); 22 | } 23 | 24 | const chainId = await createPublicClient({ 25 | transport: http(process.env.RPC_URL), 26 | }).getChainId(); 27 | const chain = chainId === arbitrum.id ? arbitrum : arbitrumSepolia; 28 | 29 | const client = createPublicClient({ 30 | chain, 31 | transport: http(process.env.RPC_URL), 32 | }); 33 | 34 | const multicallAbi = parseAbi([ 35 | "function getEthBalance(address) view returns (uint256)", 36 | ]); 37 | const tokenAbi = parseAbi([ 38 | "function balanceOf(address) view returns (uint256)", 39 | ]); 40 | 41 | const tokenAddress = 42 | chainId === arbitrum.id 43 | ? "0x1337420dED5ADb9980CFc35f8f2B054ea86f8aB1" 44 | : "0x24f9C46d86c064a6FA2a568F918fe62fC6917B3c"; 45 | 46 | const ethMulticalls = ethBalanceWallets.map( 47 | (address) => 48 | ({ 49 | address: client.chain.contracts.multicall3.address, 50 | abi: multicallAbi, 51 | functionName: "getEthBalance", 52 | args: [address], 53 | }) as const, 54 | ); 55 | const tokenMulticalls = sqdBalanceWallets.map( 56 | (address) => 57 | ({ 58 | address: tokenAddress, 59 | abi: tokenAbi, 60 | functionName: "balanceOf", 61 | args: [address], 62 | }) as const, 63 | ); 64 | 65 | // start express server with /metrics endpoint 66 | const app = express(); 67 | const port = process.env.PORT ?? 3000; 68 | 69 | app.get("/metrics", async (_, res) => { 70 | res.set("Content-Type", promClient.register.contentType); 71 | res.end(await promClient.register.metrics()); 72 | }); 73 | 74 | app.listen(port, () => { 75 | console.log(`Server listening at ${port}`); 76 | }); 77 | 78 | const balanceGauge = new promClient.Gauge({ 79 | name: "sqd_wallet_balance", 80 | help: "Balance of the wallet", 81 | labelNames: ["wallet", "token"], 82 | }); 83 | 84 | const updateMetrics = async () => { 85 | const balances = 86 | (await client 87 | .multicall({ 88 | contracts: [...ethMulticalls, ...tokenMulticalls], 89 | }) 90 | .catch(console.error)) ?? []; 91 | const timestamp = new Date().toISOString(); 92 | let i = 0; 93 | for (const balance of balances) { 94 | if (balance.status === "success") { 95 | if (i < ethMulticalls.length) { 96 | balanceGauge.set( 97 | { wallet: ethBalanceWallets[i], token: "ETH" }, 98 | Number(formatEther(balance.result)), 99 | ); 100 | } else { 101 | balanceGauge.set( 102 | { wallet: sqdBalanceWallets[i - ethMulticalls.length], token: "SQD" }, 103 | Number(formatEther(balance.result)), 104 | ); 105 | } 106 | } 107 | i++; 108 | } 109 | setTimeout( 110 | updateMetrics, 111 | 1000 * 60 * Number(process.env.INTERVAL_MINUTES ?? 120), 112 | ); 113 | }; 114 | 115 | void updateMetrics(); 116 | -------------------------------------------------------------------------------- /packages/balance-monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "preserveConstEnums": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "skipLibCheck": true 15 | }, 16 | "ts-node": { 17 | "esm": true, 18 | "experimentalSpecifierResolution": "node" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/contract-admin-state/.env.example: -------------------------------------------------------------------------------- 1 | VITE_ALCHEMY_API_KEY= -------------------------------------------------------------------------------- /packages/contract-admin-state/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | 11 | /coverage 12 | 13 | # vite 14 | 15 | dist 16 | dist-ssr 17 | 18 | # production 19 | 20 | /build 21 | 22 | # misc 23 | 24 | .DS_Store 25 | \*.pem 26 | *.local 27 | 28 | # debug 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | .pnpm-debug.log* 34 | 35 | # local env files 36 | 37 | .env 38 | .env\*.local 39 | 40 | # vercel 41 | 42 | .vercel 43 | 44 | # typescript 45 | 46 | \*.tsbuildinfo 47 | next-env.d.ts 48 | -------------------------------------------------------------------------------- /packages/contract-admin-state/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies = false -------------------------------------------------------------------------------- /packages/contract-admin-state/README.md: -------------------------------------------------------------------------------- 1 | This is a [wagmi](https://wagmi.sh) + [Vite](https://vitejs.dev/) project bootstrapped with [`create-wagmi`](https://github.com/wagmi-dev/wagmi/tree/main/packages/create-wagmi) 2 | 3 | # Getting Started 4 | 5 | Run `pnpm run dev` in your terminal, and then open [localhost:5173](http://localhost:5173) in your browser. 6 | 7 | Once the webpage has loaded, changes made to files inside the `src/` directory (e.g. `src/App.tsx`) will automatically update the webpage. 8 | 9 | # Learn more 10 | 11 | To learn more about [Vite](https://vitejs.dev/) or [wagmi](https://wagmi.sh), check out the following resources: 12 | 13 | - [wagmi Documentation](https://wagmi.sh) – learn about wagmi Hooks and API. 14 | - [wagmi Examples](https://wagmi.sh/examples/connect-wallet) – a suite of simple examples using wagmi. 15 | - [Vite Documentation](https://vitejs.dev/) – learn about Vite features and API. 16 | -------------------------------------------------------------------------------- /packages/contract-admin-state/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subsquid/subsquid-network-contracts/23cfcce34ce344ccf68ef550bbc09a5b0a0ec6d0/packages/contract-admin-state/assets/logo.png -------------------------------------------------------------------------------- /packages/contract-admin-state/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Subsquid contract state 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/contract-admin-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@subsquid-network/contract-admin-state", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node_modules/.bin/vite", 7 | "build": "node_modules/.bin/tsc && vite build", 8 | "preview": "node_modules/.bin/vite preview", 9 | "predeploy": "pnpm run build", 10 | "deploy": "gh-pages -d dist", 11 | "lint:fix": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^5.36.2", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "viem": "^2.10.9", 18 | "wagmi": "^2.9.2" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.9", 22 | "@types/react-dom": "^18.0.3", 23 | "@vitejs/plugin-react": "^4.0.0", 24 | "autoprefixer": "^10.4.16", 25 | "gh-pages": "^6.0.0", 26 | "postcss": "^8.4.31", 27 | "prettier": "^3.0.3", 28 | "prettier-plugin-tailwindcss": "^0.5.6", 29 | "tailwindcss": "^3.3.3", 30 | "typescript": "^5.4.5", 31 | "vite": "^5.2.11" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/contract-admin-state/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/contract-admin-state/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier-plugin-tailwindcss"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/config/contracts.ts: -------------------------------------------------------------------------------- 1 | import Deployments from "../../../contracts/deployments/42161.json"; 2 | import rewardsDistributionAbi from "../../../contracts/artifacts/DistributedRewardDistribution.sol/DistributedRewardsDistribution"; 3 | import stakingAbi from "../../../contracts/artifacts/Staking.sol/Staking"; 4 | import networkControllerAbi from "../../../contracts/artifacts/NetworkController.sol/NetworkController"; 5 | import gatewayRegistryAbi from "../../../contracts/artifacts/GatewayRegistry.sol/GatewayRegistry"; 6 | import rewardCalculationAbi from "../../../contracts/artifacts/RewardCalculation.sol/RewardCalculation"; 7 | import type { Address } from "viem"; 8 | import { arbitrum } from "wagmi/chains"; 9 | 10 | export const distributorContractConfig = { 11 | address: Deployments.DistributedRewardsDistribution as Address, 12 | abi: rewardsDistributionAbi.abi, 13 | chainId: arbitrum.id, 14 | }; 15 | 16 | export const networkControllerContractConfig = { 17 | address: Deployments.NetworkController as Address, 18 | abi: networkControllerAbi.abi, 19 | chainId: arbitrum.id, 20 | }; 21 | 22 | export const gatewayRegistryConfig = { 23 | address: Deployments.GatewayRegistry as Address, 24 | abi: gatewayRegistryAbi.abi, 25 | chainId: arbitrum.id, 26 | }; 27 | 28 | export const stakingContractConfig = { 29 | address: Deployments.Staking as Address, 30 | abi: stakingAbi.abi, 31 | chainId: arbitrum.id, 32 | }; 33 | 34 | export const rewardCalcContractConfig = { 35 | address: Deployments.RewardCalculation as Address, 36 | abi: rewardCalculationAbi.abi, 37 | chainId: arbitrum.id, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/hooks/useMulticall.ts: -------------------------------------------------------------------------------- 1 | import { useReadContracts } from "wagmi"; 2 | import { 3 | distributorContractConfig, 4 | networkControllerContractConfig, 5 | rewardCalcContractConfig, 6 | stakingContractConfig, 7 | } from "../config/contracts"; 8 | 9 | export function useMulticall() { 10 | const { data, ...rest } = useReadContracts({ 11 | allowFailure: false, 12 | contracts: [ 13 | { 14 | ...distributorContractConfig, 15 | functionName: "lastBlockRewarded", 16 | }, 17 | { 18 | ...distributorContractConfig, 19 | functionName: "roundRobinBlocks", 20 | }, 21 | { 22 | ...distributorContractConfig, 23 | functionName: "windowSize", 24 | }, 25 | { 26 | ...distributorContractConfig, 27 | functionName: "requiredApproves", 28 | }, 29 | { 30 | ...networkControllerContractConfig, 31 | functionName: "nextEpoch", 32 | }, 33 | { 34 | ...networkControllerContractConfig, 35 | functionName: "epochLength", 36 | }, 37 | { 38 | ...networkControllerContractConfig, 39 | functionName: "bondAmount", 40 | }, 41 | { 42 | ...networkControllerContractConfig, 43 | functionName: "storagePerWorkerInGb", 44 | }, 45 | { 46 | ...networkControllerContractConfig, 47 | functionName: "targetCapacityGb", 48 | }, 49 | { 50 | ...networkControllerContractConfig, 51 | functionName: "yearlyRewardCapCoefficient", 52 | }, 53 | { 54 | ...stakingContractConfig, 55 | functionName: "maxDelegations", 56 | }, 57 | { 58 | ...rewardCalcContractConfig, 59 | functionName: "currentApy", 60 | }, 61 | { 62 | ...rewardCalcContractConfig, 63 | functionName: "apyCap", 64 | }, 65 | { 66 | ...rewardCalcContractConfig, 67 | functionName: "effectiveTVL", 68 | }, 69 | ], 70 | }); 71 | 72 | if (!data) { 73 | return { 74 | distributor: {}, 75 | networkController: {}, 76 | staking: {}, 77 | rewardCalc: {}, 78 | }; 79 | } 80 | 81 | const [ 82 | lastBlockRewarded, 83 | roundRobinBlocks, 84 | windowSize, 85 | requiredApproves, 86 | nextEpoch, 87 | epochLength, 88 | bondAmount, 89 | storagePerWorkerInGb, 90 | targetCapacityGb, 91 | yearlyRewardCapCoefficient, 92 | maxDelegations, 93 | currentApy, 94 | apyCap, 95 | effectiveTVL, 96 | ] = data; 97 | 98 | return { 99 | distributor: { 100 | lastBlockRewarded, 101 | roundRobinBlocks, 102 | windowSize, 103 | requiredApproves, 104 | }, 105 | networkController: { 106 | nextEpoch, 107 | epochLength, 108 | bondAmount, 109 | storagePerWorkerInGb, 110 | targetCapacityGb, 111 | yearlyRewardCapCoefficient, 112 | }, 113 | staking: { 114 | maxDelegations, 115 | }, 116 | rewardCalc: { 117 | currentApy, 118 | apyCap, 119 | effectiveTVL, 120 | }, 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | 5 | import { App } from "./App"; 6 | import { config } from "./wagmi"; 7 | import { WagmiProvider } from "wagmi"; 8 | 9 | const queryClient = new QueryClient(); 10 | 11 | ReactDOM.createRoot(document.getElementById("root")!).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/utils/formatToken.ts: -------------------------------------------------------------------------------- 1 | import { toNumber } from "./toNumber"; 2 | import { formatEther } from "viem"; 3 | 4 | export const formatToken = (amount?: bigint) => 5 | amount !== undefined ? `${toNumber(formatEther(amount))} SQD` : ""; 6 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/utils/toNumber.ts: -------------------------------------------------------------------------------- 1 | export const toNumber = (value?: number | bigint | string) => { 2 | if (value === undefined) return ""; 3 | return Intl.NumberFormat("ru-RU").format(Number(value)); 4 | }; 5 | 6 | export function fromBip(value?: number | string | bigint) { 7 | if (value === undefined) return ""; 8 | return toNumber(Number(value) / 100).toString() + "%"; 9 | } 10 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/contract-admin-state/src/wagmi.ts: -------------------------------------------------------------------------------- 1 | import { arbitrum, mainnet } from "wagmi/chains"; 2 | import { createConfig, http } from "wagmi"; 3 | 4 | export const config = createConfig({ 5 | chains: [arbitrum, mainnet], 6 | connectors: [], 7 | transports: { 8 | [mainnet.id]: http(), 9 | [arbitrum.id]: http(), 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/contract-admin-state/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /packages/contract-admin-state/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "react-jsx", 9 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "noEmit": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "target": "ESNext", 17 | "useDefineForClassFields": true 18 | }, 19 | "include": [ 20 | "./src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/contract-admin-state/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | define: { 7 | global: "globalThis", 8 | }, 9 | resolve: { 10 | alias: { 11 | process: "process/browser", 12 | util: "util", 13 | }, 14 | }, 15 | plugins: [react()], 16 | }); 17 | -------------------------------------------------------------------------------- /packages/contracts/.dockerignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | node_modules 3 | lib 4 | README.md 5 | -------------------------------------------------------------------------------- /packages/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | artifacts 3 | cache 4 | node_modules 5 | .idea 6 | deployments/localhost 7 | .turbo 8 | *.pem 9 | -------------------------------------------------------------------------------- /packages/contracts/DEPLOY_TO_MAINNET.md: -------------------------------------------------------------------------------- 1 | Re-deploy a single contract to the mainnet 2 | 3 | Network controller 4 | ```bash 5 | forge create src/NetworkController.sol:NetworkController \ 6 | --private-key=$(op read "op://Shared/SQD Contract deployer/password") \ 7 | -r https://arb1.arbitrum.io/rpc \ 8 | --verify \ 9 | --broadcast \ 10 | --constructor-args 100 19810800 0 100000000000000000000000 "[0x36E2B147Db67E76aB67a4d07C293670EbeFcAE4E,0x237Abf43bc51fd5c50d0D598A1A4c26E56a8A2A0,0xB31a0D39D2C69Ed4B28d96E12cbf52C5f9Ac9a51,0x8A90A1cE5fa8Cf71De9e6f76B7d3c0B72feB8c4b]" 11 | ``` 12 | 13 | 14 | ```bash 15 | forge create src/MerkleDistributor.sol:MerkleDistributor \ 16 | --verify \ 17 | --broadcast \ 18 | --private-key "$(op read 'op://Shared/SQD Contract deployer/password')" \ 19 | --rpc-url https://arb1.arbitrum.io/rpc \ 20 | --constructor-args "0x1337420dED5ADb9980CFc35f8f2B054ea86f8aB1" "0xf7aa507d6d5599cfa7604c1194fbffec5d422a4bb76223ca14b75dc030e3a163" "0x24B97D7eE13Abc7c1fc109Ea66CabdcBe3ADe1a7" 21 | ``` -------------------------------------------------------------------------------- /packages/contracts/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = edrevo/dockerfile-plus 2 | 3 | INCLUDE+ Dockerfile.common 4 | 5 | ENTRYPOINT ["/bin/sh", "-c"] 6 | -------------------------------------------------------------------------------- /packages/contracts/Dockerfile-playground: -------------------------------------------------------------------------------- 1 | # syntax = edrevo/dockerfile-plus 2 | 3 | INCLUDE+ Dockerfile.common 4 | 5 | ENTRYPOINT ["sh", "./spinup-testnet.sh"] 6 | -------------------------------------------------------------------------------- /packages/contracts/Dockerfile-reclaimFunds: -------------------------------------------------------------------------------- 1 | # syntax = edrevo/dockerfile-plus 2 | 3 | INCLUDE+ Dockerfile.common 4 | 5 | ENTRYPOINT ["forge", "script", "script/UnlockFunds.s.sol", "--broadcast", "--rpc-url", "arbitrum-sepolia"] 6 | -------------------------------------------------------------------------------- /packages/contracts/Dockerfile-registerGateway: -------------------------------------------------------------------------------- 1 | # syntax = edrevo/dockerfile-plus 2 | 3 | INCLUDE+ Dockerfile.common 4 | 5 | ENTRYPOINT ["./scripts/register-gateway.mjs"] 6 | -------------------------------------------------------------------------------- /packages/contracts/Dockerfile.common: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/foundry-rs/foundry:latest 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add --update jq python3 py3-pip nodejs-current npm 6 | RUN pip install base58 7 | COPY foundry.toml ./foundry.toml 8 | COPY package.json ./package.json 9 | RUN npm install -g zx 10 | RUN npm install @libp2p/peer-id 11 | RUN forge install foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts@v5.0.1 OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.2 PaulRBerg/prb-math@release-v4 --no-git 12 | COPY . . 13 | RUN forge build 14 | 15 | -------------------------------------------------------------------------------- /packages/contracts/b58.py: -------------------------------------------------------------------------------- 1 | import base58 2 | import sys 3 | 4 | print(base58.b58decode(sys.argv[1]).hex()) 5 | -------------------------------------------------------------------------------- /packages/contracts/deploy/hardhat/00_SQD.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | const ethers = hre.ethers; 3 | 4 | const func = async function ({ deployments, getNamedAccounts }) { 5 | const { deploy } = deployments; 6 | const { deployer } = await getNamedAccounts(); 7 | 8 | console.log("Deployer address:", deployer); // Add this line to check the deployer's address 9 | 10 | const tSQD = await deploy("tSQD", { 11 | from: deployer, 12 | args: [[deployer], [100]], // 100% allocation to the deployer 13 | log: true, 14 | }); 15 | 16 | const tSQDContract = await ethers.getContractAt("tSQD", tSQD.address); 17 | const amount = ethers.utils.parseUnits("150000", 18); 18 | 19 | const signers = await ethers.getSigners(); 20 | 21 | for (let i = 1; i <= 10; i++) { 22 | await tSQDContract.transfer(signers[i].address, amount); 23 | console.log(`Transferred 150000 tSQD to: ${signers[i].address}`); 24 | } 25 | 26 | }; 27 | 28 | module.exports = func; 29 | func.tags = ["tSQD"]; 30 | -------------------------------------------------------------------------------- /packages/contracts/deploy/hardhat/01_WorkerRegistration.js: -------------------------------------------------------------------------------- 1 | // deploy/01_WorkerRegistration.js 2 | const hre = require("hardhat"); 3 | const { ethers } = hre; 4 | const { utils } = ethers; 5 | 6 | function delay(ms) { 7 | return new Promise((resolve) => setTimeout(resolve, ms)); 8 | } 9 | 10 | const func = async function ({ deployments, getNamedAccounts }) { 11 | const { deploy } = deployments; 12 | const { deployer } = await getNamedAccounts(); 13 | 14 | // Get the deployed tSQD contract address 15 | const tSQD = await deployments.get("tSQD"); 16 | const tSQDAddress = tSQD.address; 17 | console.log("tSQD deployed at:", tSQDAddress); 18 | 19 | // Access the current network name and epochLengthBlocks value from the Hardhat configuration 20 | const networkName = hre.network.name; 21 | const epochLengthBlocks = hre.config.networks[networkName].epochLengthBlocks; 22 | 23 | console.log(`Network name: ${networkName}, epoch length blocks: ${epochLengthBlocks}, chainId: ${hre.config.networks[networkName].chainId}`) 24 | // Deploy the WorkerRegistration contract using the tSQD contract address 25 | console.log(`Waiting for new block`) 26 | await delay(1000); 27 | const salt = utils.id("worker-registration-salt"); 28 | 29 | const workerRegistration = await deploy("WorkerRegistration", { 30 | from: deployer, 31 | args: [tSQDAddress, epochLengthBlocks], 32 | log: true, 33 | deterministicDeployment: salt, 34 | }); 35 | if (networkName === "arbitrum-goerli") { 36 | await deploy("WorkerRegistrationFacade", { 37 | from: deployer, 38 | args: ["0xA7E47a7aE0FB29BeF4485f6CAb2ee1b85c1D38aB"], 39 | log: true, 40 | deterministicDeployment: salt, 41 | }); 42 | } 43 | 44 | console.log("WorkerRegistration deployed at:", workerRegistration.address); 45 | }; 46 | 47 | module.exports = func; 48 | func.tags = ["WorkerRegistration"]; 49 | func.dependencies = ["tSQD"]; 50 | 51 | 52 | -------------------------------------------------------------------------------- /packages/contracts/deploy/hardhat/02_RewardCalculation.js: -------------------------------------------------------------------------------- 1 | // deploy/01_WorkerRegistration.js 2 | const hre = require("hardhat"); 3 | const { ethers } = hre; 4 | const { utils } = ethers; 5 | 6 | function delay(ms) { 7 | return new Promise((resolve) => setTimeout(resolve, ms)); 8 | } 9 | 10 | const func = async function ({ deployments, getNamedAccounts }) { 11 | const { deploy } = deployments; 12 | const { deployer } = await getNamedAccounts(); 13 | 14 | // Get the deployed tSQD contract address 15 | const workerRegistration = await deployments.get("WorkerRegistration"); 16 | 17 | const salt = utils.id("reward-calculation-salt"); 18 | 19 | const rewardCalculation = await deploy("RewardCalculation", { 20 | from: deployer, 21 | args: [workerRegistration.address], 22 | log: true, 23 | deterministicDeployment: salt, 24 | }); 25 | 26 | console.log("RewardCalculation deployed at:", rewardCalculation.address); 27 | }; 28 | 29 | module.exports = func; 30 | func.tags = ["WorkerRegistration"]; 31 | func.dependencies = ["tSQD"]; 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/contracts/deploy/hardhat/03_RewardsDistribution.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | const { ethers } = hre; 3 | const { utils } = ethers; 4 | 5 | const func = async function ({ deployments, getNamedAccounts }) { 6 | const { deploy } = deployments; 7 | const { deployer } = await getNamedAccounts(); 8 | 9 | // Get the deployed tSQD contract address 10 | const workerRegistration = await deployments.get("WorkerRegistration"); 11 | const tsqd = await deployments.get("tSQD"); 12 | 13 | const salt = utils.id("reward-distribution-salt"); 14 | 15 | const rewardDistribution = await deploy("RewardsDistribution", { 16 | from: deployer, 17 | args: [deployer, workerRegistration.address], 18 | log: true, 19 | deterministicDeployment: salt, 20 | }); 21 | const distributionContract = await ethers.getContractAt( 22 | "RewardsDistribution", 23 | rewardDistribution.address, 24 | ); 25 | await distributionContract.grantRole( 26 | await distributionContract.REWARDS_DISTRIBUTOR_ROLE(), 27 | deployer, 28 | ); 29 | const rewardTreasury = await deploy("RewardTreasury", { 30 | from: deployer, 31 | args: [deployer, tsqd.address], 32 | log: true, 33 | deterministicDeployment: salt, 34 | }); 35 | await distributionContract.grantRole( 36 | await distributionContract.REWARDS_TREASURY_ROLE(), 37 | rewardTreasury.address, 38 | ); 39 | console.log("RewardCalculation deployed at:", rewardDistribution.address); 40 | }; 41 | 42 | module.exports = func; 43 | func.tags = ["WorkerRegistration"]; 44 | func.dependencies = ["SQD"]; 45 | -------------------------------------------------------------------------------- /packages/contracts/deployments/42161.json: -------------------------------------------------------------------------------- 1 | { 2 | "SQD": "0x1337420dED5ADb9980CFc35f8f2B054ea86f8aB1", 3 | "RouterImplementation": "0x4a7c41397f623ca04b60a59bcaa77346aeae86aa", 4 | "Router": "0x67F56D27dab93eEb07f6372274aCa277F49dA941 ", 5 | "NetworkController": "0x4cf58097d790b193d22ed633bf8b15c9bc4f0da7", 6 | "Staking": "0xb31a0d39d2c69ed4b28d96e12cbf52c5f9ac9a51", 7 | "WorkerRegistration": "0x36e2b147db67e76ab67a4d07c293670ebefcae4e", 8 | "RewardTreasury": "0x237abf43bc51fd5c50d0d598a1a4c26e56a8a2a0", 9 | "DistributedRewardsDistribution": "0x4de282bD18aE4987B3070F4D5eF8c80756362AEa", 10 | "GatewayRegistryImplementation": "0x2591121581d2a7022cd3f66f1a7ccc9560df2152", 11 | "GatewayRegistry": "0x8a90a1ce5fa8cf71de9e6f76b7d3c0b72feb8c4b", 12 | "EqualStrategy": "0xa604f84c9c59e223b12c831b35723aa0d7277f8b", 13 | "SubequalStrategy": "0xf197094d96f45325ee8bd2c43c5d25c05d66ab62", 14 | "AllocationsViewer": "0x88ce6d8d70df9fe049315fd9d6c3d59108c15c4c", 15 | "SoftCap": "0x0eb27b1cbba04698dd7ce0f2364584d33a616545", 16 | "RewardCalculation": "0xd3d2c185a30484641c07b60e7d952d7b85516eb5", 17 | "VestingFactory": "0x1f8f83cd76baeca1cb5c064ad59203c82b4e4ece", 18 | "TemporaryHoldingFactory": "0x14926ebf05a904b8e2e2bf05c10ecca9a54d8d0d" 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/deployments/421613.json: -------------------------------------------------------------------------------- 1 | { 2 | "tSQD": "0x8e86cDD6b5252b45305106071db047563F00e0a8", 3 | "ProxyAdmin": "0xF6733AB90988c457a5Ac360D7f8dfB9E24aA108F", 4 | "Router": "0x455DE73b2Ddf008E53b4D274A0F0b065bB3C7023", 5 | "TransparentUpgradeableProxy": "0x387534CcCc6073c98aB343B6D261ef9598e659AB", 6 | "NetworkController": "0xA07d7e04b13f71e8E8a094B2477c405f255F750e", 7 | "Staking": "0x76F1BB25627d47c30A5a6fCb62B2Af4ff97b4702", 8 | "WorkerRegistration": "0x6D02ABC5fa967B0b085083F5Cf36c24502447244", 9 | "RewardTreasury": "0x97fAFd95bc0A332aA6123A8f8f369dfc492ff1D0", 10 | "DistributedRewardsDistribution": "0x9Be48cB9Eb443E850316DD09cdF1c2E150b09245", 11 | "GatewayRegistry": "0x5244E221ab9A63aB5471dA1B6BFdC00F72f0eA74", 12 | "VestingFactory": "0x2fe9Dfa9FaF3Ebcc293Df4832BCAd687999CD63E", 13 | "RewardCalculation": "0x78F7ddBB09D77f08b8E6a3Df94E79fe606966d82" 14 | } -------------------------------------------------------------------------------- /packages/contracts/deployments/421614.json: -------------------------------------------------------------------------------- 1 | { 2 | "RouterImplementation": "0x35c773A9EB697433Bd9972f66Ebf1D9Bcb1cD5d3", 3 | "Router": "0xD2093610c5d27c201CD47bCF1Df4071610114b64", 4 | "NetworkController": "0x68Fc7E375945d8C8dFb0050c337Ff09E962D976D", 5 | "Staking": "0x347E326b8b4EA27c87d5CA291e708cdEC6d65EB5", 6 | "WorkerRegistration": "0xCD8e983F8c4202B0085825Cf21833927D1e2b6Dc", 7 | "RewardTreasury": "0x785136e611E15D532C36502AaBdfE8E35008c7ca", 8 | "DistributedRewardsDistribution": "0x68f9fE3504652360afF430dF198E1Cb7B2dCfD57", 9 | "GatewayRegistryImplementation": "0x66e5724e7Ee1efD950c3Ae60D81bb63d38e6f892", 10 | "GatewayRegistry": "0xAB46F688AbA4FcD1920F21E9BD16B229316D8b0a", 11 | "EqualStrategy": "0x94DF0410BF415765e8e9431d545AF9805859b5Db", 12 | "SubequalStrategy": "0x20cA692986D127CE78938E2518cE2F49F105eC48", 13 | "SoftCap": "0x52f31c9c019f840A9C0e74F66ACc95455B254BeA", 14 | "RewardCalculation": "0x93D16d5210122c804DE9931b41b3c6FA2649CE3F", 15 | "AllocationsViewer": "0xC0Af6432947db51e0C179050dAF801F19d40D2B7", 16 | "SQD": "0x24f9C46d86c064a6FA2a568F918fe62fC6917B3c", 17 | "tSQDL1": "0xb0571a833fc49442c030e27295f33049d9e5443b" 18 | } 19 | -------------------------------------------------------------------------------- /packages/contracts/deployments/arbitrum-goerli/.chainId: -------------------------------------------------------------------------------- 1 | 421613 2 | -------------------------------------------------------------------------------- /packages/contracts/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | anvil: 5 | image: ghcr.io/foundry-rs/foundry:latest 6 | ports: 7 | - "8545:8545" 8 | command: anvil 9 | environment: 10 | ANVIL_IP_ADDR: 0.0.0.0 11 | 12 | deploy: 13 | image: testnet 14 | environment: 15 | PRIVATE_KEY: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 16 | RPC_URL: http://anvil:8545 17 | depends_on: 18 | - anvil 19 | 20 | -------------------------------------------------------------------------------- /packages/contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "artifacts" 4 | evm_version = "shanghai" 5 | libs = ["lib"] 6 | remappings = [ 7 | "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", 8 | "openzeppelin-contracts/contracts=lib/openzeppelin-contracts/contracts/", 9 | "@prb/math=lib/prb-math/", 10 | ] 11 | fs_permissions = [{ access = "read-write", path = "./"}] 12 | 13 | [fmt] 14 | int_types = "long" 15 | tab_width = 2 16 | 17 | [rpc_endpoints] 18 | arbitrum-goerli = "https://goerli-rollup.arbitrum.io/rpc" 19 | arbitrum-sepolia = "https://sepolia-rollup.arbitrum.io/rpc" 20 | arbitrum = "https://arbitrum-mainnet.infura.io/v3/aa959c8b01204689b1cceaf56c4d4297" 21 | 22 | [etherscan] 23 | arbitrum-goerli = { key = "N89QM8KDR8SZ52I7YCSN6Z35QAUFUYBHBV" } 24 | arbitrum-sepolia = { key = "N89QM8KDR8SZ52I7YCSN6Z35QAUFUYBHBV" } 25 | arbitrum = { key = "N89QM8KDR8SZ52I7YCSN6Z35QAUFUYBHBV" } 26 | sepolia = { key = "MMGCJXPZ3J392VFETA9Y9425IB41A6FWXW" } 27 | 28 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 29 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@subsquid-network/contracts", 3 | "type": "module", 4 | "version": "0.0.3", 5 | "bin": { 6 | "subsquid-network-register": "packages/contracts/scripts/register-worker.js" 7 | }, 8 | "dependencies": { 9 | "@openzeppelin/merkle-tree": "^1.0.6", 10 | "bs58": "^5.0.0", 11 | "dotenv": "^16.0.3", 12 | "ethers": "^5.7.2", 13 | "hardhat": "^2.14.0", 14 | "peer-id": "^0.16.0" 15 | }, 16 | "scripts": { 17 | "run:dev": "hardhat node --watch", 18 | "test": "forge test -vv", 19 | "lint": "forge fmt --check", 20 | "lint:fix": "forge fmt", 21 | "deploy-tokens": "ts-node scripts/deploy-tokens.ts", 22 | "deploy": "TOKEN=0x24f9C46d86c064a6FA2a568F918fe62fC6917B3c forge script script/Deploy.s.sol --broadcast --verify --json --rpc-url arbitrum-sepolia && node scripts/deployment-json.js", 23 | "register-worker": "forge script script/RegisterWorker.s.sol --broadcast --json --rpc-url arbitrum-sepolia", 24 | "list-workers": "node -r dotenv/config scripts/active-workers.js", 25 | "build": "forge build --out artifacts && npm run build:types", 26 | "build:types": "find artifacts -name \"*.json\" -exec bash -c \"echo export default \\ > {}.ts && cat {} >> {}.ts\" \\; && find artifacts -name \"*.json.ts\" -exec bash -c 'mv $0 ${0%???????}ts' {} \\;", 27 | "bounty": "docker run -v $PWD:/app ghcr.io/foundry-rs/foundry:latest 'forge script /app/script/Bounty.s.sol --root /app --rpc-url arbitrum-sepolia --broadcast'" 28 | }, 29 | "devDependencies": { 30 | "@arbitrum/sdk": "^3.1.13", 31 | "@nomiclabs/hardhat-ethers": "^2.2.3", 32 | "@nomiclabs/hardhat-waffle": "^2.0.5", 33 | "@types/chai": "^4.3.4", 34 | "@types/mocha": "^10.0.1", 35 | "@types/node": "^18.15.11", 36 | "chai": "^4.3.4", 37 | "ethereum-waffle": "^3.4.4", 38 | "hardhat-deploy": "^0.11.26", 39 | "sinon": "^15", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^5.0.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/contracts/register-gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #if ! [[ "$PRIVATE_KEY" =~ ^0x[0-9a-fA-F]{64}$ ]]; then 4 | # echo "PRIVATE_KEY is not set or invalid" 5 | # exit 1 6 | #fi 7 | #if ! [[ "$GATEWAY_ID" =~ ^12D3.*$ ]]; then 8 | # echo "GATEWAY_ID is not set or invalid" 9 | # exit 1 10 | #fi 11 | ./scripts/toPeerId.mjs 12 | 13 | #echo "You will stake ${STAKE_AMOUNT:-100} tSQD for ${STAKE_DURATION:-180} days" 14 | # 15 | #GATEWAY_ID=$(python3 b58.py $GATEWAY_ID) forge script script/RegisterGateway.s.sol --broadcast --json --rpc-url arbitrum-sepolia 16 | -------------------------------------------------------------------------------- /packages/contracts/script/Bounty.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "forge-std/StdJson.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "../src/VestingFactory.sol"; 8 | 9 | struct WorkerCreator { 10 | address payable wallet; 11 | uint256 workerCount; 12 | } 13 | 14 | uint256 constant ETHER_AMOUNT = 0.005 ether; 15 | uint256 constant BOND_AMOUNT = 100_000 ether; 16 | 17 | contract Bounty is Script { 18 | using stdJson for string; 19 | 20 | function run() public { 21 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 22 | address sender = vm.addr(deployerPrivateKey); 23 | IERC20 token = IERC20(address(0x24f9C46d86c064a6FA2a568F918fe62fC6917B3c)); 24 | VestingFactory factory = VestingFactory(address(0x0eD5FB811167De1928322a0fa30Ed7F3c8C370Ca)); 25 | require(token.balanceOf(sender) > 0, "No SQD for the sender"); 26 | string memory root = vm.projectRoot(); 27 | string memory path = string.concat(root, "/script/workersSurvey.json"); 28 | string memory json = vm.readFile(path); 29 | bytes memory data = json.parseRaw("."); 30 | WorkerCreator[] memory workers = abi.decode(data, (WorkerCreator[])); 31 | vm.startBroadcast(deployerPrivateKey); 32 | for (uint256 i = 0; i < workers.length; i++) { 33 | uint256 amount = BOND_AMOUNT * workers[i].workerCount; 34 | SubsquidVesting vesting = factory.createVesting( 35 | workers[i].wallet, 36 | 1705273200, // Jan 15, 2024 37 | 60 days, 38 | 0, 39 | amount 40 | ); 41 | token.transfer(address(vesting), BOND_AMOUNT * workers[i].workerCount); 42 | } 43 | vm.stopBroadcast(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/contracts/script/CreateVestings.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "forge-std/StdJson.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "../src/VestingFactory.sol"; 8 | 9 | struct Entry { 10 | uint256 Amount; 11 | uint256 Cliff; 12 | uint64 End; 13 | uint64 Start; 14 | address Wallet; 15 | } 16 | 17 | contract CreateVestings is Script { 18 | using stdJson for string; 19 | 20 | function run() public { 21 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 22 | address sender = vm.addr(deployerPrivateKey); 23 | VestingFactory factory = VestingFactory(address(0x0eD5FB811167De1928322a0fa30Ed7F3c8C370Ca)); 24 | string memory root = vm.projectRoot(); 25 | string memory path = string.concat(root, "/script/vestings.json"); 26 | string memory json = vm.readFile(path); 27 | bytes memory data = json.parseRaw("."); 28 | Entry[] memory entries = abi.decode(data, (Entry[])); 29 | vm.startBroadcast(deployerPrivateKey); 30 | vm.writeFile("vestings.csv", "Wallet,Vesting\n"); 31 | for (uint256 i = 0; i < entries.length; i++) { 32 | SubsquidVesting vesting = factory.createVesting( 33 | entries[i].Wallet, 34 | entries[i].Start, 35 | entries[i].End - entries[i].Start, 36 | entries[i].Cliff, 37 | entries[i].Amount * 1 ether 38 | ); 39 | vm.writeLine("vestings.csv", string.concat(vm.toString(entries[i].Wallet), ",", vm.toString(address(vesting)))); 40 | } 41 | vm.stopBroadcast(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/contracts/script/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 8 | 9 | import "../src/NetworkController.sol"; 10 | import "../src/Staking.sol"; 11 | import "../src/WorkerRegistration.sol"; 12 | import "../src/RewardTreasury.sol"; 13 | import "../src/RewardCalculation.sol"; 14 | import "../src/DistributedRewardDistribution.sol"; 15 | import "../src/SQD.sol"; 16 | import "../src/Router.sol"; 17 | import "../src/GatewayRegistry.sol"; 18 | import "../src/VestingFactory.sol"; 19 | import "../src/SoftCap.sol"; 20 | import "../src/gateway-strategies/EqualStrategy.sol"; 21 | import "../src/AllocationsViewer.sol"; 22 | import "../src/gateway-strategies/SubequalStrategy.sol"; 23 | 24 | contract Deploy is Script { 25 | function run() public { 26 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 27 | vm.startBroadcast(deployerPrivateKey); 28 | 29 | SQD token = SQD(vm.envOr("TOKEN", address(0))); 30 | if (address(token) == address(0)) { 31 | address[] memory recipients = new address[](1); 32 | recipients[0] = vm.addr(deployerPrivateKey); 33 | uint256[] memory amounts = new uint256[](1); 34 | amounts[0] = 1337 * (10 ** 6) * 1 ether; 35 | token = new SQD(recipients, amounts, IL1CustomGateway(address(0)), IGatewayRouter2(address(0))); 36 | } 37 | 38 | Router router = 39 | Router(address(new TransparentUpgradeableProxy(address(new Router()), vm.addr(deployerPrivateKey), ""))); 40 | 41 | NetworkController network = new NetworkController(100, 0, 0, 100000 ether, new address[](0)); 42 | Staking staking = new Staking(token, router); 43 | WorkerRegistration workerRegistration = new WorkerRegistration(token, router); 44 | RewardTreasury treasury = new RewardTreasury(token); 45 | DistributedRewardsDistribution distributor = new DistributedRewardsDistribution(router); 46 | GatewayRegistry gatewayReg = GatewayRegistry( 47 | address(new TransparentUpgradeableProxy(address(new GatewayRegistry()), vm.addr(deployerPrivateKey), "")) 48 | ); 49 | gatewayReg.initialize(IERC20WithMetadata(address(token)), router); 50 | EqualStrategy strategy = new EqualStrategy(router, gatewayReg); 51 | SubequalStrategy subStrategy = new SubequalStrategy(router, gatewayReg); 52 | SoftCap cap = new SoftCap(router); 53 | RewardCalculation rewardCalc = new RewardCalculation(router, cap); 54 | AllocationsViewer viewer = new AllocationsViewer(gatewayReg); 55 | router.initialize(workerRegistration, staking, address(treasury), network, rewardCalc); 56 | staking.grantRole(staking.REWARDS_DISTRIBUTOR_ROLE(), address(distributor)); 57 | treasury.setWhitelistedDistributor(distributor, true); 58 | distributor.grantRole(distributor.REWARDS_TREASURY_ROLE(), address(treasury)); 59 | 60 | network.setAllowedVestedTarget(address(workerRegistration), true); 61 | network.setAllowedVestedTarget(address(staking), true); 62 | network.setAllowedVestedTarget(address(gatewayReg), true); 63 | network.setAllowedVestedTarget(address(treasury), true); 64 | 65 | gatewayReg.setIsStrategyAllowed(address(strategy), true, true); 66 | gatewayReg.setIsStrategyAllowed(address(subStrategy), true, false); 67 | 68 | vm.stopBroadcast(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/contracts/script/DeployDistributor.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 8 | 9 | import "../src/NetworkController.sol"; 10 | import "../src/Staking.sol"; 11 | import "../src/WorkerRegistration.sol"; 12 | import "../src/RewardTreasury.sol"; 13 | import "../src/RewardCalculation.sol"; 14 | import "../src/DistributedRewardDistribution.sol"; 15 | import "../src/SQD.sol"; 16 | import "../src/Router.sol"; 17 | import "../src/GatewayRegistry.sol"; 18 | import "../src/VestingFactory.sol"; 19 | import "../src/SoftCap.sol"; 20 | import "../src/gateway-strategies/EqualStrategy.sol"; 21 | import "../src/AllocationsViewer.sol"; 22 | import "../src/gateway-strategies/SubequalStrategy.sol"; 23 | 24 | contract DeployDistributor is Script { 25 | function run() public { 26 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 27 | vm.startBroadcast(deployerPrivateKey); 28 | 29 | Staking staking = Staking(0xB31a0D39D2C69Ed4B28d96E12cbf52C5f9Ac9a51); 30 | RewardTreasury treasury = RewardTreasury(0x237Abf43bc51fd5c50d0D598A1A4c26E56a8A2A0); 31 | DistributedRewardsDistribution oldDistributor = 32 | DistributedRewardsDistribution(0xab690dA5815659Fe94f08F73E870D91a4d376d8f); 33 | DistributedRewardsDistribution distributor = new DistributedRewardsDistribution(oldDistributor.router()); 34 | staking.grantRole(staking.REWARDS_DISTRIBUTOR_ROLE(), address(distributor)); 35 | staking.revokeRole(staking.REWARDS_DISTRIBUTOR_ROLE(), address(oldDistributor)); 36 | treasury.setWhitelistedDistributor(distributor, true); 37 | treasury.setWhitelistedDistributor(oldDistributor, false); 38 | distributor.grantRole(distributor.REWARDS_TREASURY_ROLE(), address(treasury)); 39 | 40 | vm.stopBroadcast(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/contracts/script/RedeployGateways.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 8 | 9 | import "../src/NetworkController.sol"; 10 | import "../src/Staking.sol"; 11 | import "../src/WorkerRegistration.sol"; 12 | import "../src/RewardTreasury.sol"; 13 | import "../src/RewardCalculation.sol"; 14 | import "../src/DistributedRewardDistribution.sol"; 15 | import "../src/SQD.sol"; 16 | import "../src/Router.sol"; 17 | import "../src/GatewayRegistry.sol"; 18 | import "../src/VestingFactory.sol"; 19 | import "../src/SoftCap.sol"; 20 | import "../src/gateway-strategies/EqualStrategy.sol"; 21 | import "../src/AllocationsViewer.sol"; 22 | 23 | contract Deploy is Script { 24 | address proxyAdmin = 0xcC33ac93745811b320F2DCe730bFd1ec94599F5d; 25 | Router router = Router(0x6bAc05cDe58D02953496541b4d615f71a5Db57a3); 26 | IERC20WithMetadata token = IERC20WithMetadata(0x24f9C46d86c064a6FA2a568F918fe62fC6917B3c); 27 | 28 | function run() public { 29 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 30 | vm.startBroadcast(deployerPrivateKey); 31 | 32 | GatewayRegistry gatewayReg = 33 | GatewayRegistry(address(new TransparentUpgradeableProxy(address(new GatewayRegistry()), proxyAdmin, ""))); 34 | gatewayReg.initialize(token, router); 35 | EqualStrategy strategy = new EqualStrategy(router, gatewayReg); 36 | gatewayReg.setIsStrategyAllowed(address(strategy), true, true); 37 | AllocationsViewer viewer = new AllocationsViewer(gatewayReg); 38 | 39 | vm.stopBroadcast(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/contracts/script/RegisterGateway.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/GatewayRegistry.sol"; 6 | 7 | contract RegisterGateway is Script { 8 | function run() public { 9 | uint256 deployerPrivateKey = vm.envOr("PRIVATE_KEY", uint256(0)); 10 | bytes memory peerId = vm.envOr("GATEWAY_ID", bytes("")); 11 | if (deployerPrivateKey == 0) { 12 | console2.log("PRIVATE_KEY env var is required"); 13 | return; 14 | } 15 | if (peerId.length == 0) { 16 | console2.log("GATEWAY_ID env var is required"); 17 | return; 18 | } 19 | uint256 stakeAmount = vm.envOr("STAKE_AMOUNT", uint256(100)) * 1 ether; 20 | uint128 duration = uint128(vm.envOr("STAKE_DURATION", uint256(180))); 21 | GatewayRegistry gatewayReg = 22 | GatewayRegistry(vm.envOr("GATEWAY_REGISTRY", address(0xC168fD9298141E3a19c624DF5692ABeeb480Fb94))); 23 | IERC20 token = gatewayReg.token(); 24 | vm.startBroadcast(deployerPrivateKey); 25 | if (gatewayReg.getGateway(peerId).operator == address(0)) { 26 | gatewayReg.register(peerId); 27 | } else { 28 | console2.log("Gateway already registered"); 29 | } 30 | token.approve(address(gatewayReg), stakeAmount); 31 | if (gatewayReg.getStake(gatewayReg.getGateway(peerId).operator).amount > 0) { 32 | gatewayReg.addStake(stakeAmount); 33 | } else { 34 | gatewayReg.stake(stakeAmount, duration); 35 | } 36 | vm.stopBroadcast(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/contracts/script/RegisterWorker.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/WorkerRegistration.sol"; 6 | 7 | contract RegisterWorker is Script { 8 | function run() public { 9 | uint256 deployerPrivateKey = vm.envOr("PRIVATE_KEY", uint256(0)); 10 | bytes memory peerId = vm.envOr("WORKER_ID", bytes("")); 11 | WorkerRegistration workerRegistration = 12 | WorkerRegistration(vm.envOr("WORKER_REGISTRATION", address(0x7Bf0B1ee9767eAc70A857cEbb24b83115093477F))); 13 | if (deployerPrivateKey == 0) { 14 | console2.log("PRIVATE_KEY env var is required"); 15 | return; 16 | } 17 | if (peerId.length == 0) { 18 | console2.log("WORKER_ID env var is required"); 19 | return; 20 | } 21 | IERC20 token = workerRegistration.SQD(); 22 | vm.startBroadcast(deployerPrivateKey); 23 | token.approve(address(workerRegistration), workerRegistration.bondAmount()); 24 | workerRegistration.register(peerId); 25 | vm.stopBroadcast(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/contracts/scripts/createMerkleTree.ts: -------------------------------------------------------------------------------- 1 | import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; 2 | import fs from "fs"; 3 | import { utils } from "ethers"; 4 | 5 | const walletMap: any = {}; 6 | 7 | function parseCsvLine(text: string, index: number): [number, string, string] { 8 | const parts = text.split(","); 9 | if (parts.length !== 2) { 10 | throw new Error(`Invalid CSV line at index ${index}`); 11 | } 12 | return [ 13 | index + 1, 14 | parts[0].toLowerCase(), 15 | utils.parseEther(parts[1].trim()).toString(), 16 | ] as [number, string, string]; 17 | } 18 | 19 | const leaves = fs 20 | .readFileSync("./airdrop.csv") 21 | .toString() 22 | .split("\n") 23 | .filter((line) => line) 24 | .map(parseCsvLine); 25 | 26 | console.log(`Generating leaves for ${leaves.length} wallets.`); 27 | let total = 0n; 28 | 29 | for (const leaf of leaves) { 30 | const wallet = leaf[1]; 31 | if (walletMap[wallet]) { 32 | throw new Error(`Duplicate wallet ${wallet}`); 33 | } 34 | walletMap[wallet] = leaf; 35 | total += BigInt(leaf[2]); 36 | } 37 | 38 | fs.mkdirSync('airdrop-data', { recursive: true }); 39 | fs.writeFileSync("airdrop-data/leaves.json", JSON.stringify(walletMap)); 40 | console.log(`Finished generating leaves.json.`); 41 | console.log(`Total distribution: ${total} (${total / (10n ** 18n)} SQD)`); 42 | 43 | const tree = StandardMerkleTree.of(leaves, ["uint32", "address", "uint256"]); 44 | 45 | console.log(`Finished creating MerkleTree with ${leaves.length} leaves.`); 46 | console.log(`Root: ${tree.root}`); 47 | 48 | fs.writeFileSync("airdrop-data/tree.json", JSON.stringify(tree.dump())); 49 | -------------------------------------------------------------------------------- /packages/contracts/scripts/fordefi/request.ts: -------------------------------------------------------------------------------- 1 | export function fordefiRequest( 2 | to: string, 3 | amount: string, 4 | name: string, 5 | chain: "mainnet" | "sepolia", 6 | tokenAddr: string, 7 | ) { 8 | return { 9 | signer_type: "api_signer", 10 | type: "evm_transaction", 11 | details: { 12 | type: "evm_transfer", 13 | to, 14 | gas: { 15 | type: "priority", 16 | priority_level: "low", 17 | }, 18 | chain: chain === "sepolia" ? "arbitrum_sepolia" : "arbitrum_mainnet", 19 | asset_identifier: { 20 | type: "evm", 21 | details: { 22 | type: "erc20", 23 | token: { 24 | chain: 25 | chain === "sepolia" 26 | ? "evm_arbitrum_sepolia" 27 | : "evm_arbitrum_mainnet", 28 | hex_repr: tokenAddr, 29 | }, 30 | }, 31 | }, 32 | value: { 33 | type: "value", 34 | value: amount, 35 | }, 36 | }, 37 | note: name, 38 | vault_id: process.env.FORDEFI_VAULT_ID, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/contracts/scripts/fordefi/sendTransaction.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import fs from "fs"; 3 | 4 | const gatewayHost = "api.fordefi.com"; 5 | 6 | async function waitForFordefiTransaction(id: string) { 7 | const accessToken = process.env.ACCESS_TOKEN; 8 | const path = "/api/v1/transactions"; 9 | 10 | let timeout = 250; 11 | while (true) { 12 | const response = await fetch(`https://${gatewayHost}${path}/${id}`, { 13 | method: "GET", 14 | headers: { 15 | Accept: "application/json", 16 | Authorization: `Bearer ${accessToken}`, 17 | }, 18 | }); 19 | const json = await response.json(); 20 | if (json.hash && json.mined_result?.reversion?.state === "not_reverted") { 21 | return json.hash; 22 | } 23 | if (json.hash && json.mined_result?.reversion?.reason) { 24 | throw new Error( 25 | JSON.stringify({ 26 | id: json.hash, 27 | reason: json.mined_result?.reason, 28 | }), 29 | ); 30 | } 31 | if (timeout >= 30000) { 32 | throw new Error(`Transaction ${id} timeout`); 33 | } 34 | timeout *= 2; 35 | await new Promise((resolve) => setTimeout(resolve, timeout)); 36 | } 37 | } 38 | 39 | export async function sendFordefiTransaction(request: any): Promise { 40 | const accessToken = process.env.FORDEFI_ACCESS_TOKEN; 41 | 42 | const requestBody = JSON.stringify(request); 43 | const path = "/api/v1/transactions"; 44 | const privateKeyFile = "./private.pem"; 45 | const timestamp = new Date().getTime(); 46 | const payload = `${path}|${timestamp}|${requestBody}`; 47 | 48 | const secretPem = fs.readFileSync(privateKeyFile, "utf8"); 49 | const privateKey = crypto.createPrivateKey(secretPem); 50 | const sign = crypto.createSign("SHA256").update(payload, "utf8").end(); 51 | const signature = sign.sign(privateKey, "base64"); 52 | 53 | const response = await fetch(`https://${gatewayHost}${path}`, { 54 | method: "POST", 55 | headers: { 56 | "Content-Type": "application/json", 57 | Authorization: `Bearer ${accessToken}`, 58 | "X-Timestamp": timestamp.toString(), 59 | "X-Signature": signature, 60 | }, 61 | body: requestBody, 62 | }); 63 | 64 | if (!response.ok) { 65 | throw new Error(await response.text()); 66 | } 67 | 68 | const { id } = await response.json(); 69 | return id; 70 | } 71 | -------------------------------------------------------------------------------- /packages/contracts/scripts/sendVaultTokens.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { ethers } from "ethers"; 3 | import SubsquidVesting from "../artifacts/Vesting.sol/SubsquidVesting"; 4 | import { fordefiRequest } from "./fordefi/request"; 5 | import { sendFordefiTransaction } from "./fordefi/sendTransaction"; 6 | 7 | if ( 8 | process.env.Network && 9 | process.env.Network !== "sepolia" && 10 | process.env.Network !== "mainnet" 11 | ) { 12 | throw new Error("Invalid network. Only sepolia and mainnet are supported."); 13 | } 14 | 15 | const network: "sepolia" | "mainnet" = 16 | (process.env.NETWORK as any) || "mainnet"; 17 | 18 | const rpc = { 19 | sepolia: "https://sepolia-rollup.arbitrum.io/rpc", 20 | mainnet: "https://arb1.arbitrum.io/rpc", 21 | }[network]; 22 | 23 | const tokenAddress = { 24 | sepolia: "0x24f9C46d86c064a6FA2a568F918fe62fC6917B3c", 25 | mainnet: "0x1337420dED5ADb9980CFc35f8f2B054ea86f8aB1", 26 | }[network]; 27 | 28 | async function sendVaultTokens() { 29 | const data = fs 30 | .readFileSync("./vestings.csv") 31 | .toString() 32 | .split("\n") 33 | .filter((line) => line) 34 | .slice(1) 35 | .map((line) => line.split(",").slice(0, 2) as [string, string]); 36 | const provider = new ethers.providers.JsonRpcProvider(rpc); 37 | let i = 0; 38 | const total = data.length; 39 | for (const [wallet, vesting] of data) { 40 | const vestingContract = new ethers.Contract( 41 | vesting, 42 | SubsquidVesting.abi, 43 | provider, 44 | ); 45 | const balance = await vestingContract.balanceOf(tokenAddress); 46 | if (balance.gt(0)) { 47 | console.log( 48 | `Vesting ${vesting} has ${ethers.utils.formatEther(balance)} SQD, skipping [${++i}/${total}]`, 49 | ); 50 | continue; 51 | } 52 | const amount = await vestingContract.expectedTotalAmount(); 53 | const vestingStart = await vestingContract.start(); 54 | const end = await vestingContract.end(); 55 | const release = await vestingContract.immediateReleaseBIP(); 56 | const beneficiary = await vestingContract.owner(); 57 | if (beneficiary.toLowerCase() !== wallet.toLowerCase()) { 58 | throw new Error( 59 | `Beneficiary ${beneficiary} is not equal to wallet ${wallet} for vesting ${vesting}`, 60 | ); 61 | } 62 | const name = ` 63 | Wallet ${wallet} 64 | Vesting ${vesting} 65 | Amount : ${ethers.utils.formatEther(amount)} SQD 66 | Immidiate release : ${ethers.utils.formatEther(amount.mul(release).div(10_000))} SQD (${release.toNumber() / 100}%) 67 | Vesting start : ${new Date(vestingStart.toNumber() * 1000).toUTCString()} 68 | Vesting end : ${new Date(end.toNumber() * 1000).toUTCString()}`; 69 | const request = fordefiRequest( 70 | vesting, 71 | amount.toString(), 72 | name, 73 | network, 74 | tokenAddress, 75 | ); 76 | await sendFordefiTransaction(request); 77 | console.log( 78 | "Sent transaction to fordefi for", 79 | vesting, 80 | `[${++i}/${total}]`, 81 | ); 82 | } 83 | } 84 | 85 | void sendVaultTokens(); 86 | -------------------------------------------------------------------------------- /packages/contracts/spinup-testnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | forge build 4 | forge script script/Deploy.s.sol --broadcast --json --rpc-url $RPC_URL 5 | forge script script/PreparePlayground.s.sol --tc PreparePlayground --broadcast --json --rpc-url $RPC_URL 6 | echo "Deployed contract addresses" 7 | jq '[.transactions[] | select(.transactionType == "CREATE")] | map({contractName, contractAddress})' ./broadcast/Deploy.s.sol/31337/run-latest.json 8 | jq '[.transactions[] | select(.transactionType == "CREATE")] | map({contractName, contractAddress})' ./broadcast/PreparePlayground.s.sol/31337/run-latest.json 9 | 10 | echo "Register workers" 11 | 12 | WORKER_ID=$(python3 b58.py $WORKER1_ID) forge script script/RegisterWorker.s.sol --broadcast --rpc-url $RPC_URL 13 | WORKER_ID=$(python3 b58.py $WORKER2_ID) forge script script/RegisterWorker.s.sol --broadcast --rpc-url $RPC_URL 14 | 15 | echo "Register pings collector (hack)" 16 | WORKER_ID=$(python3 b58.py $PINGS_COLLECTOR_ID) forge script script/RegisterWorker.s.sol --broadcast --rpc-url $RPC_URL 17 | 18 | echo "Register gateways" 19 | 20 | GATEWAY_ID=$(python3 b58.py $GATEWAY1_ID) forge script script/RegisterGateway.s.sol --broadcast --rpc-url $RPC_URL 21 | cast rpc --rpc-url $RPC_URL anvil_mine 0xff 0x00 22 | GATEWAY_ID=$(python3 b58.py $GATEWAY2_ID) forge script script/RegisterGateway.s.sol --broadcast --rpc-url $RPC_URL 23 | -------------------------------------------------------------------------------- /packages/contracts/src/AccessControlledPausable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/utils/Pausable.sol"; 5 | import "@openzeppelin/contracts/access/AccessControl.sol"; 6 | 7 | /// @dev abstract contract that allows wallets with special pauser role to pause contracts 8 | abstract contract AccessControlledPausable is Pausable, AccessControl { 9 | bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); 10 | 11 | constructor() { 12 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 13 | _grantRole(PAUSER_ROLE, msg.sender); 14 | } 15 | 16 | function pause() public virtual onlyRole(PAUSER_ROLE) { 17 | _pause(); 18 | } 19 | 20 | function unpause() public virtual onlyRole(PAUSER_ROLE) { 21 | _unpause(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/contracts/src/AllocationsViewer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "./interfaces/IGatewayRegistry.sol"; 5 | import "./interfaces/IGatewayStrategy.sol"; 6 | 7 | contract AllocationsViewer { 8 | IGatewayRegistry public gatewayRegistry; 9 | 10 | constructor(IGatewayRegistry _gatewayRegistry) { 11 | gatewayRegistry = _gatewayRegistry; 12 | } 13 | 14 | struct Allocation { 15 | bytes gatewayId; 16 | uint256 allocated; 17 | address operator; 18 | } 19 | 20 | function getAllocations(uint256 workerId, uint256 pageNumber, uint256 perPage) 21 | external 22 | view 23 | returns (Allocation[] memory) 24 | { 25 | bytes[] memory gateways = gatewayRegistry.getActiveGateways(pageNumber, perPage); 26 | Allocation[] memory allocs = new Allocation[](gateways.length); 27 | for (uint256 i = 0; i < gateways.length; i++) { 28 | IGatewayStrategy strategy = IGatewayStrategy(gatewayRegistry.getUsedStrategy(gateways[i])); 29 | if (address(strategy) != address(0)) { 30 | uint256 cus = strategy.computationUnitsPerEpoch(gateways[i], workerId); 31 | address operator = gatewayRegistry.getGateway(gateways[i]).operator; 32 | allocs[i] = Allocation(gateways[i], cus, operator); 33 | } 34 | } 35 | return allocs; 36 | } 37 | 38 | function getGatewayCount() external view returns (uint256) { 39 | return gatewayRegistry.getActiveGatewaysCount(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/contracts/src/Executable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/utils/Address.sol"; 6 | import "./interfaces/IRouter.sol"; 7 | 8 | /// @dev Abstract contract that can execute arbitrary calldata 9 | abstract contract Executable { 10 | using Address for address; 11 | 12 | IERC20 public SQD; 13 | IRouter public router; 14 | /// @dev Amount of SQD deposited into protocol through this contract, used to calculate total vesting balance 15 | uint256 public depositedIntoProtocol; 16 | 17 | function _canExecute(address executor) internal view virtual returns (bool); 18 | 19 | function execute(address to, bytes calldata data) external { 20 | execute(to, data, 0); 21 | } 22 | 23 | /** 24 | * @dev Execute arbitrary calldata 25 | * @param to Target address, must be allowed by network controller 26 | * @param data Calldata to execute 27 | * @param requiredApprove Amount of SQD to approve before transaction. If 0, no approval is done 28 | * In case of SQD balance change, depositedIntoProtocol is updated 29 | */ 30 | function execute(address to, bytes calldata data, uint256 requiredApprove) public returns (bytes memory) { 31 | require(_canExecute(msg.sender), "Not allowed to execute"); 32 | require(router.networkController().isAllowedVestedTarget(to), "Target is not allowed"); 33 | 34 | // It's not likely that following addresses will be allowed by network controller, but just in case 35 | require(to != address(this), "Cannot call self"); 36 | require(to != address(SQD), "Cannot call SQD"); 37 | 38 | if (requiredApprove > 0) { 39 | SQD.approve(to, requiredApprove); 40 | } 41 | depositedIntoProtocol += SQD.balanceOf(address(this)); 42 | bytes memory result = to.functionCall(data); 43 | uint256 balanceAfter = SQD.balanceOf(address(this)); 44 | if (balanceAfter > depositedIntoProtocol) { 45 | depositedIntoProtocol = 0; 46 | } else { 47 | depositedIntoProtocol -= balanceAfter; 48 | } 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/contracts/src/LinearToSqrtCap.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import {UD60x18, ud, convert} from "@prb/math/src/UD60x18.sol"; 4 | import {SD59x18, sd} from "@prb/math/src/SD59x18.sol"; 5 | import "./interfaces/IRouter.sol"; 6 | import "@openzeppelin/contracts/access/AccessControl.sol"; 7 | 8 | /* 9 | * @dev Softly cap effective stake on workers 10 | * The contract is used to limit the ability of stakers to heavily influence the reward distribution 11 | * The cap function is designed to be near linear for small values and to approach 1/3 for large values 12 | * So the weight of delegations never exceeds 1/3 of the total stake 13 | */ 14 | contract LinearToSqrtCap is AccessControl { 15 | IRouter public router; 16 | uint256 public linearEnd; 17 | UD60x18 public sqrtCoefficient; 18 | 19 | constructor(IRouter _router) { 20 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 21 | 22 | router = _router; 23 | setLinearEnd(20000 ether); 24 | } 25 | 26 | function setLinearEnd(uint256 _linearEnd) public onlyRole(DEFAULT_ADMIN_ROLE) { 27 | linearEnd = _linearEnd; 28 | sqrtCoefficient = convert(_linearEnd).sqrt(); 29 | } 30 | 31 | /// @dev Get caped stake of a worker (should be not more than bond / 3) 32 | function capedStake(uint256 workerId) public view returns (uint256) { 33 | return _capStake(_getStake(workerId)); 34 | } 35 | 36 | /// @dev How will the stake change after delegation 37 | /// In case of unstake, delegation can be negative 38 | function capedStakeAfterDelegation(uint256 workerId, int256 delegationAmount) public view returns (uint256) { 39 | int256 stakeAfterDelegation = int256(_getStake(workerId)) + delegationAmount; 40 | if (stakeAfterDelegation < 0) { 41 | return 0; 42 | } 43 | return _capStake(uint256(stakeAfterDelegation)); 44 | } 45 | 46 | function _getStake(uint256 workerId) internal view returns (uint256) { 47 | return router.staking().delegated(workerId); 48 | } 49 | 50 | function _capStake(uint256 stake) internal view returns (uint256) { 51 | if (stake <= linearEnd) { 52 | return stake; 53 | } 54 | return convert(convert(stake).sqrt() * sqrtCoefficient); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/contracts/src/MerkleDistributor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | contract MerkleDistributor { 8 | IERC20 public immutable token; 9 | bytes32 public immutable merkleRoot; 10 | address public immutable owner; 11 | 12 | event Claimed(uint256 indexed index, address indexed account, uint256 amount); 13 | event Ended(); 14 | 15 | // This is a packed array of booleans. 16 | mapping(uint256 => uint256) private claimedBitMap; 17 | 18 | constructor(IERC20 token_, bytes32 merkleRoot_, address owner_) { 19 | token = token_; 20 | merkleRoot = merkleRoot_; 21 | owner = owner_; 22 | } 23 | 24 | function isClaimed(uint256 index) public view returns (bool) { 25 | uint256 claimedWordIndex = index / 256; 26 | uint256 claimedBitIndex = index % 256; 27 | uint256 claimedWord = claimedBitMap[claimedWordIndex]; 28 | uint256 mask = (1 << claimedBitIndex); 29 | return claimedWord & mask == mask; 30 | } 31 | 32 | function _setClaimed(uint256 index) private { 33 | uint256 claimedWordIndex = index / 256; 34 | uint256 claimedBitIndex = index % 256; 35 | claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex); 36 | } 37 | 38 | function claim(uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof) external { 39 | require(!isClaimed(index), "MerkleDistributor: Drop already claimed."); 40 | 41 | // Verify the merkle proof. 42 | bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, account, amount)))); 43 | require(MerkleProof.verifyCalldata(merkleProof, merkleRoot, leaf), "MerkleDistributor: Invalid proof."); 44 | 45 | // Mark it claimed and send the token. 46 | _setClaimed(index); 47 | require(token.transfer(account, amount), "MerkleDistributor: Transfer failed."); 48 | 49 | emit Claimed(index, account, amount); 50 | } 51 | 52 | function withdrawAll() external { 53 | require(msg.sender == owner, "MerkleDistributor: Only owner can withdraw"); 54 | require(token.transfer(owner, token.balanceOf(address(this))), "MerkleDistributor: Transfer failed."); 55 | 56 | emit Ended(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/contracts/src/RewardCalculation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 5 | 6 | import "./interfaces/IRouter.sol"; 7 | import "./SoftCap.sol"; 8 | 9 | /** 10 | * @title Reward Calculation Contract 11 | * @dev Contract that calculates rewards for workers and stakers 12 | * For more info, see https://github.com/subsquid/subsquid-network-contracts/wiki/Whitepaper#appendix-ii----rewards 13 | * @notice Functions in the contract are expected to be used by view functions as effectiveTVL is a heavy operation 14 | */ 15 | contract RewardCalculation is IRewardCalculation { 16 | using SafeCast for uint256; 17 | using SafeCast for int256; 18 | 19 | IRouter public immutable router; 20 | SoftCap public immutable stakeCap; 21 | uint256 public constant INITIAL_REWARD_POOL_SIZE = 120_330_000 ether; 22 | 23 | constructor(IRouter _router, SoftCap _stakeCap) { 24 | router = _router; 25 | stakeCap = _stakeCap; 26 | } 27 | 28 | /// @dev APY based on target and actual storages 29 | /// smoothed base_apr function from [here](https://github.com/subsquid/subsquid-network-contracts/wiki/Whitepaper#reward-rate) 30 | function baseApr(uint256 target, uint256 actual) public pure returns (uint256) { 31 | int256 uRate = (target.toInt256() - actual.toInt256()) * 10000 / target.toInt256(); 32 | if (uRate >= 9000) { 33 | return 7000; 34 | } 35 | if (uRate >= 0) { 36 | return 2500 + uRate.toUint256() / 2; 37 | } 38 | int256 resultApy = 2000 + uRate / 20; 39 | if (resultApy < 0) { 40 | return 0; 41 | } 42 | return resultApy.toUint256(); 43 | } 44 | 45 | function apyCap() public view returns (uint256) { 46 | uint256 tvl = effectiveTVL(); 47 | if (tvl == 0) { 48 | return 10000; 49 | } 50 | return router.networkController().yearlyRewardCapCoefficient() * INITIAL_REWARD_POOL_SIZE / effectiveTVL(); 51 | } 52 | 53 | function apy(uint256 target, uint256 actual) public view returns (uint256) { 54 | uint256 base = baseApr(target, actual); 55 | uint256 maxApy = apyCap(); 56 | if (base > maxApy) { 57 | return maxApy; 58 | } 59 | return base; 60 | } 61 | 62 | function effectiveTVL() public view returns (uint256) { 63 | uint256 workerCount = router.workerRegistration().getActiveWorkerCount(); 64 | uint256 bond = router.networkController().bondAmount(); 65 | uint256 bondStaked = workerCount * bond; 66 | uint256 effectiveStake = 0; 67 | uint256[] memory activeWorkers = router.workerRegistration().getActiveWorkerIds(); 68 | for (uint256 i = 0; i < activeWorkers.length; i++) { 69 | effectiveStake += stakeCap.capedStake(activeWorkers[i]); 70 | } 71 | return effectiveStake + bondStaked; 72 | } 73 | 74 | /// @return current APY for a worker with targetGb storage 75 | function currentApy() public view returns (uint256) { 76 | return apy( 77 | router.networkController().targetCapacityGb(), 78 | router.workerRegistration().getActiveWorkerCount() * router.networkController().storagePerWorkerInGb() 79 | ); 80 | } 81 | 82 | /// @return reword for an epoch that lasted epochLengthInSeconds seconds 83 | function epochReward(uint256 epochLengthInSeconds) public view returns (uint256) { 84 | return currentApy() * effectiveTVL() * epochLengthInSeconds / 365 days / 10000; 85 | } 86 | 87 | /// @return bonus to allocations for the tokens staked by gateway 88 | /// @notice result is in basis points 89 | function boostFactor(uint256 duration) public pure returns (uint256) { 90 | if (duration < 60 days) { 91 | return 10000; 92 | } 93 | if (duration < 180 days) { 94 | return 10000 + (duration - 30 days) / 30 days * 2000; 95 | } 96 | if (duration < 360 days) { 97 | return 20000; 98 | } 99 | if (duration < 720 days) { 100 | return 25000; 101 | } 102 | return 30000; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/contracts/src/RewardTreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import "./interfaces/IRewardsDistribution.sol"; 7 | import "./AccessControlledPausable.sol"; 8 | 9 | /** 10 | * @title Reward Treasury Contract 11 | * @dev Contract that stores rewards for workers and stakers and has a list of whitelisted distributors that it can claim from 12 | */ 13 | contract RewardTreasury is AccessControlledPausable { 14 | mapping(IRewardsDistribution => bool) public isWhitelistedDistributor; 15 | IERC20 public immutable rewardToken; 16 | 17 | /// @dev Emitted when rewards are claimed 18 | event Claimed(address indexed by, address indexed receiver, uint256 amount); 19 | /// @dev Emitted when distributor is whitelisted or removed from whitelist 20 | event WhitelistedDistributorSet(IRewardsDistribution indexed distributor, bool isWhitelisted); 21 | 22 | /** 23 | * @dev Constructor 24 | * @param _rewardToken address of the SQD token 25 | */ 26 | constructor(IERC20 _rewardToken) { 27 | rewardToken = _rewardToken; 28 | } 29 | 30 | /** 31 | * @dev Claim rewards from distributor and send rewards to the caller 32 | * @param rewardDistribution address of the rewards distribution contract 33 | * rewardDistribution must be whitelisted by admin 34 | */ 35 | function claim(IRewardsDistribution rewardDistribution) external { 36 | _claim(rewardDistribution, msg.sender); 37 | } 38 | 39 | /** 40 | * @dev Claim rewards from distributor and send to receiver 41 | * @param rewardDistribution address of the rewards distribution contract 42 | * @param receiver address that receives funds 43 | */ 44 | function claimFor(IRewardsDistribution rewardDistribution, address receiver) external { 45 | _claim(rewardDistribution, receiver); 46 | } 47 | 48 | /// @return how much can be claimed by sender from rewardDistribution 49 | function claimable(IRewardsDistribution rewardDistribution, address sender) external view returns (uint256) { 50 | return rewardDistribution.claimable(sender); 51 | } 52 | 53 | /** 54 | * @dev Set distributor as whitelisted or not 55 | * @param distributor address of the rewards distribution contract 56 | * @param isWhitelisted whether the distributor is whitelisted or not 57 | * can only be called by admin 58 | */ 59 | function setWhitelistedDistributor(IRewardsDistribution distributor, bool isWhitelisted) 60 | external 61 | onlyRole(DEFAULT_ADMIN_ROLE) 62 | { 63 | isWhitelistedDistributor[distributor] = isWhitelisted; 64 | 65 | emit WhitelistedDistributorSet(distributor, isWhitelisted); 66 | } 67 | 68 | function _claim(IRewardsDistribution rewardDistribution, address receiver) internal whenNotPaused { 69 | require(isWhitelistedDistributor[rewardDistribution], "Distributor not whitelisted"); 70 | uint256 reward = rewardDistribution.claim(msg.sender); 71 | rewardToken.transfer(receiver, reward); 72 | 73 | emit Claimed(msg.sender, receiver, reward); 74 | } 75 | 76 | /// @dev Reclaim all funds from the contract in case of emergency 77 | function reclaimFunds() external onlyRole(DEFAULT_ADMIN_ROLE) { 78 | rewardToken.transfer(msg.sender, rewardToken.balanceOf(address(this))); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/contracts/src/Router.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts/access/AccessControl.sol"; 6 | import "./interfaces/IRouter.sol"; 7 | 8 | /** 9 | * @title Router Contract 10 | * @dev Contract that holds addresses of crucial subsquid contracts 11 | * Contract is designed to be upgradeable 12 | * All setters can only be called by admin 13 | */ 14 | contract Router is Initializable, AccessControl, IRouter { 15 | IWorkerRegistration public workerRegistration; 16 | IStaking public staking; 17 | address public rewardTreasury; 18 | INetworkController public networkController; 19 | IRewardCalculation public rewardCalculation; 20 | 21 | event WorkerRegistrationSet(IWorkerRegistration workerRegistration); 22 | event StakingSet(IStaking staking); 23 | event RewardTreasurySet(address rewardTreasury); 24 | event NetworkControllerSet(INetworkController networkController); 25 | event RewardCalculationSet(IRewardCalculation rewardCalculation); 26 | 27 | function initialize( 28 | IWorkerRegistration _workerRegistration, 29 | IStaking _staking, 30 | address _rewardTreasury, 31 | INetworkController _networkController, 32 | IRewardCalculation _rewardCalculation 33 | ) external initializer { 34 | _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); 35 | 36 | workerRegistration = _workerRegistration; 37 | staking = _staking; 38 | rewardTreasury = _rewardTreasury; 39 | networkController = _networkController; 40 | rewardCalculation = _rewardCalculation; 41 | } 42 | 43 | function setWorkerRegistration(IWorkerRegistration _workerRegistration) external onlyRole(DEFAULT_ADMIN_ROLE) { 44 | workerRegistration = _workerRegistration; 45 | 46 | emit WorkerRegistrationSet(_workerRegistration); 47 | } 48 | 49 | function setStaking(IStaking _staking) external onlyRole(DEFAULT_ADMIN_ROLE) { 50 | staking = _staking; 51 | 52 | emit StakingSet(_staking); 53 | } 54 | 55 | function setRewardTreasury(address _rewardTreasury) external onlyRole(DEFAULT_ADMIN_ROLE) { 56 | rewardTreasury = _rewardTreasury; 57 | 58 | emit RewardTreasurySet(_rewardTreasury); 59 | } 60 | 61 | function setNetworkController(INetworkController _networkController) external onlyRole(DEFAULT_ADMIN_ROLE) { 62 | networkController = _networkController; 63 | 64 | emit NetworkControllerSet(_networkController); 65 | } 66 | 67 | function setRewardCalculation(IRewardCalculation _rewardCalculation) external onlyRole(DEFAULT_ADMIN_ROLE) { 68 | rewardCalculation = _rewardCalculation; 69 | 70 | emit RewardCalculationSet(_rewardCalculation); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/contracts/src/SQD.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | interface IL1CustomGateway { 7 | function registerTokenToL2( 8 | address _l2Address, 9 | uint256 _maxGas, 10 | uint256 _gasPriceBid, 11 | uint256 _maxSubmissionCost, 12 | address _creditBackAddress 13 | ) external payable returns (uint256); 14 | } 15 | 16 | interface IGatewayRouter2 { 17 | function setGateway( 18 | address _gateway, 19 | uint256 _maxGas, 20 | uint256 _gasPriceBid, 21 | uint256 _maxSubmissionCost, 22 | address _creditBackAddress 23 | ) external payable returns (uint256); 24 | } 25 | 26 | /** 27 | * @title SQD Token 28 | * @dev Simple ERC20 token, supports arbitrum bridging 29 | */ 30 | contract SQD is ERC20 { 31 | bool internal shouldRegisterGateway; 32 | IL1CustomGateway immutable gateway; 33 | IGatewayRouter2 immutable router; 34 | 35 | constructor( 36 | address[] memory recipients, 37 | uint256[] memory mintedAmounts, 38 | IL1CustomGateway _gateway, 39 | IGatewayRouter2 _router 40 | ) ERC20("SQD Token", "SQD") { 41 | gateway = _gateway; 42 | router = _router; 43 | 44 | require(recipients.length == mintedAmounts.length, "Recipients and minted arrays must have the same length"); 45 | 46 | uint256 initialSupply = 1337 * (10 ** 6) * (10 ** decimals()); 47 | uint256 totalMinted; 48 | 49 | for (uint256 i = 0; i < recipients.length; i++) { 50 | _mint(recipients[i], mintedAmounts[i]); 51 | totalMinted += mintedAmounts[i]; 52 | } 53 | 54 | require(totalMinted == initialSupply, "Not all tokens were minted"); 55 | } 56 | 57 | /// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2` 58 | function isArbitrumEnabled() external view returns (uint8) { 59 | require(shouldRegisterGateway, "NOT_EXPECTED_CALL"); 60 | return uint8(0xb1); 61 | } 62 | 63 | function registerTokenOnL2( 64 | address l2CustomTokenAddress, 65 | uint256 maxSubmissionCostForCustomGateway, 66 | uint256 maxSubmissionCostForRouter, 67 | uint256 maxGasForCustomGateway, 68 | uint256 maxGasForRouter, 69 | uint256 gasPriceBid, 70 | uint256 valueForGateway, 71 | uint256 valueForRouter, 72 | address creditBackAddress 73 | ) public payable { 74 | require(!shouldRegisterGateway, "ALREADY_REGISTERED"); 75 | shouldRegisterGateway = true; 76 | 77 | gateway.registerTokenToL2{value: valueForGateway}( 78 | l2CustomTokenAddress, maxGasForCustomGateway, gasPriceBid, maxSubmissionCostForCustomGateway, creditBackAddress 79 | ); 80 | 81 | router.setGateway{value: valueForRouter}( 82 | address(gateway), maxGasForRouter, gasPriceBid, maxSubmissionCostForRouter, creditBackAddress 83 | ); 84 | 85 | shouldRegisterGateway = false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/contracts/src/SoftCap.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import {UD60x18, ud, convert} from "@prb/math/src/UD60x18.sol"; 4 | import {SD59x18, sd} from "@prb/math/src/SD59x18.sol"; 5 | import "./interfaces/IRouter.sol"; 6 | 7 | /* 8 | * @dev Softly cap effective stake on workers 9 | * The contract is used to limit the ability of stakers to heavily influence the reward distribution 10 | * The cap function is designd to be near linear for small values and to approach 1/3 for large values 11 | * So the weight of delegations never exceeds 1/3 of the total stake 12 | */ 13 | contract SoftCap { 14 | IRouter public router; 15 | 16 | constructor(IRouter _router) { 17 | router = _router; 18 | } 19 | 20 | /// @dev Slightly modified normal distribution 21 | /// 2/3^(x-1)^4-2/3 22 | function cap(UD60x18 x) public pure returns (UD60x18) { 23 | SD59x18 exponent = (x.intoSD59x18() - sd(1e18)).powu(4); 24 | return ((sd(2e18) / sd(3e18)).pow(exponent) - sd(2e18) / sd(3e18)).intoUD60x18(); 25 | } 26 | 27 | /// @dev Get caped stake of a worker (should be not more than bond / 3) 28 | function capedStake(uint256 workerId) public view returns (uint256) { 29 | return _capStake(_getStake(workerId)); 30 | } 31 | 32 | /// @dev How will the stake change after delegation 33 | /// In case of unstake, delegation can be negative 34 | function capedStakeAfterDelegation(uint256 workerId, int256 delegationAmount) public view returns (uint256) { 35 | int256 stakeAfterDelegation = int256(_getStake(workerId)) + delegationAmount; 36 | if (stakeAfterDelegation < 0) { 37 | return 0; 38 | } 39 | return _capStake(uint256(stakeAfterDelegation)); 40 | } 41 | 42 | function _getStake(uint256 workerId) internal view returns (uint256) { 43 | return router.staking().delegated(workerId); 44 | } 45 | 46 | function _capStake(uint256 stake) internal view returns (uint256) { 47 | uint256 bond = router.networkController().bondAmount(); 48 | uint256 total = stake + bond; 49 | UD60x18 stakingShare = convert(stake) / convert(total); 50 | return uint256(convert(cap(stakingShare) * convert(bond))); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/contracts/src/TemporaryHolding.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "./Executable.sol"; 6 | import "./interfaces/IRouter.sol"; 7 | 8 | /** 9 | * @title Temporary Holding Contract 10 | * @dev Contract that holds SQD tokens for a beneficiary to interact with the network 11 | * The tokens are unlocked after lockedUntil timestamp 12 | * The beneficiary can execute contracts, allowed by network controller through this contract 13 | */ 14 | contract TemporaryHolding is Executable { 15 | address public immutable beneficiary; 16 | address public immutable admin; 17 | uint256 public immutable lockedUntil; 18 | uint256 public immutable expectedAmount; 19 | 20 | constructor( 21 | IERC20 _SQD, 22 | IRouter _router, 23 | address _beneficiary, 24 | address _admin, 25 | uint256 _lockedUntil, 26 | uint256 _expectedAmount 27 | ) { 28 | SQD = _SQD; 29 | router = _router; 30 | beneficiary = _beneficiary; 31 | admin = _admin; 32 | lockedUntil = _lockedUntil; 33 | expectedAmount = _expectedAmount; 34 | } 35 | 36 | function release() external { 37 | require(block.timestamp >= lockedUntil, "Funds are locked"); 38 | SQD.transfer(admin, balanceOf()); 39 | } 40 | 41 | function balanceOf() public view returns (uint256) { 42 | return SQD.balanceOf(address(this)); 43 | } 44 | 45 | function _canExecute(address executor) internal view override returns (bool) { 46 | if (block.timestamp < lockedUntil) { 47 | return executor == beneficiary; 48 | } 49 | return executor == admin; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/contracts/src/TemporaryHoldingFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "./interfaces/IRouter.sol"; 6 | import "./AccessControlledPausable.sol"; 7 | import "./TemporaryHolding.sol"; 8 | 9 | /** 10 | * @title Subsquid Temporary Holding Contract Factory 11 | * @dev Contract used to deploy holding contracts 12 | */ 13 | contract TemporaryHoldingFactory is AccessControlledPausable { 14 | bytes32 public constant HOLDING_CREATOR_ROLE = keccak256("HOLDING_CREATOR_ROLE"); 15 | 16 | IERC20 public immutable token; 17 | IRouter public immutable router; 18 | 19 | event TemporaryHoldingCreated( 20 | TemporaryHolding indexed vesting, 21 | address indexed beneficiaryAddress, 22 | address indexed admin, 23 | uint64 unlockTimestamp, 24 | uint256 expectedTotalAmount 25 | ); 26 | 27 | constructor(IERC20 _token, IRouter _router) { 28 | token = _token; 29 | router = _router; 30 | _grantRole(HOLDING_CREATOR_ROLE, msg.sender); 31 | } 32 | 33 | function createTemporaryHolding( 34 | address beneficiaryAddress, 35 | address admin, 36 | uint64 unlockTimestamp, 37 | uint256 expectedTotalAmount 38 | ) external onlyRole(HOLDING_CREATOR_ROLE) whenNotPaused returns (TemporaryHolding) { 39 | TemporaryHolding holding = 40 | new TemporaryHolding(token, router, beneficiaryAddress, admin, unlockTimestamp, expectedTotalAmount); 41 | emit TemporaryHoldingCreated(holding, beneficiaryAddress, admin, unlockTimestamp, expectedTotalAmount); 42 | return holding; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/contracts/src/Vesting.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/finance/VestingWallet.sol"; 6 | 7 | import "./interfaces/IRouter.sol"; 8 | import "./Executable.sol"; 9 | 10 | /** 11 | * @title Subsquid Vesting Contract 12 | * @dev Contract that holds SQD tokens for a beneficiary 13 | * The tokens are unlocked linearly with a cliff according to _vestingSchedule 14 | * The beneficiary can execute contracts, allowed by network controller through this contract 15 | */ 16 | contract SubsquidVesting is Executable, VestingWallet { 17 | uint256 public immutable expectedTotalAmount; 18 | uint256 public immutable immediateReleaseBIP; 19 | 20 | constructor( 21 | IERC20 _SQD, 22 | IRouter _router, 23 | address beneficiaryAddress, 24 | uint64 startTimestamp, 25 | uint64 durationSeconds, 26 | uint256 _immediateReleaseBIP, 27 | uint256 _expectedTotalAmount 28 | ) VestingWallet(beneficiaryAddress, startTimestamp, durationSeconds) { 29 | SQD = _SQD; 30 | router = _router; 31 | expectedTotalAmount = _expectedTotalAmount; 32 | immediateReleaseBIP = _immediateReleaseBIP; 33 | } 34 | 35 | receive() external payable override { 36 | revert("SubsquidVesting: cannot receive Ether"); 37 | } 38 | 39 | function release() public override { 40 | release(address(SQD)); 41 | } 42 | 43 | function balanceOf(IERC20 token) public view returns (uint256) { 44 | return token.balanceOf(address(this)); 45 | } 46 | 47 | function release(address token) public override onlyOwner { 48 | require(token == address(SQD), "Only SQD is supported"); 49 | super.release(token); 50 | } 51 | 52 | function releasable(address token) public view override returns (uint256) { 53 | uint256 _releasable = super.releasable(token); 54 | uint256 currentBalance = balanceOf(IERC20(token)); 55 | if (currentBalance < _releasable) { 56 | return currentBalance; 57 | } 58 | return _releasable; 59 | } 60 | 61 | function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual override returns (uint256) { 62 | if (timestamp < start()) return 0; 63 | uint256 cliff = totalAllocation * immediateReleaseBIP / 10000; 64 | return cliff + super._vestingSchedule(totalAllocation - cliff + depositedIntoProtocol, timestamp); 65 | } 66 | 67 | function _canExecute(address executor) internal view override returns (bool) { 68 | return executor == owner(); 69 | } 70 | 71 | function _transferOwnership(address newOwner) internal override { 72 | require(owner() == address(0), "Ownership transfer is not allowed"); 73 | super._transferOwnership(newOwner); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/contracts/src/VestingFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "./interfaces/IRouter.sol"; 6 | import "./Vesting.sol"; 7 | import "./AccessControlledPausable.sol"; 8 | 9 | /** 10 | * @title Subsquid Vesting Contract Factory 11 | * @dev Contract used to deploy vesting contracts 12 | */ 13 | contract VestingFactory is AccessControlledPausable { 14 | bytes32 public constant VESTING_CREATOR_ROLE = keccak256("VESTING_CREATOR_ROLE"); 15 | 16 | IERC20 public immutable token; 17 | IRouter public immutable router; 18 | 19 | event VestingCreated( 20 | SubsquidVesting indexed vesting, 21 | address indexed beneficiary, 22 | uint64 startTimestamp, 23 | uint64 durationSeconds, 24 | uint256 expectedTotalAmount 25 | ); 26 | 27 | constructor(IERC20 _token, IRouter _router) { 28 | token = _token; 29 | router = _router; 30 | _grantRole(VESTING_CREATOR_ROLE, msg.sender); 31 | } 32 | 33 | function createVesting( 34 | address beneficiaryAddress, 35 | uint64 startTimestamp, 36 | uint64 durationSeconds, 37 | uint256 immediateReleaseBIP, 38 | uint256 expectedTotalAmount 39 | ) external onlyRole(VESTING_CREATOR_ROLE) whenNotPaused returns (SubsquidVesting) { 40 | SubsquidVesting vesting = new SubsquidVesting( 41 | token, router, beneficiaryAddress, startTimestamp, durationSeconds, immediateReleaseBIP, expectedTotalAmount 42 | ); 43 | emit VestingCreated(vesting, beneficiaryAddress, startTimestamp, durationSeconds, expectedTotalAmount); 44 | return vesting; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/contracts/src/arbitrum/SQD.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | /** 7 | * @dev 8 | * This is a simple ERC20 token implementing the IArbToken interface 9 | * See more here https://docs.arbitrum.io/devs-how-tos/bridge-tokens/how-to-bridge-tokens-generic-custom 10 | * 11 | */ 12 | contract SQDArbitrum is ERC20 { 13 | address public immutable l2Gateway; 14 | address public immutable l1Address; 15 | 16 | modifier onlyL2Gateway() { 17 | require(msg.sender == l2Gateway, "NOT_GATEWAY"); 18 | _; 19 | } 20 | 21 | constructor(address _l2Gateway, address _l1TokenAddress) ERC20("SQD Token", "SQD") { 22 | l2Gateway = _l2Gateway; 23 | l1Address = _l1TokenAddress; 24 | } 25 | 26 | /** 27 | * @notice should increase token supply by amount, and should only be callable by the L2Gateway. 28 | */ 29 | function bridgeMint(address account, uint256 amount) external onlyL2Gateway { 30 | _mint(account, amount); 31 | } 32 | 33 | /** 34 | * @notice should decrease token supply by amount, and should only be callable by the L2Gateway. 35 | */ 36 | function bridgeBurn(address account, uint256 amount) external onlyL2Gateway { 37 | _burn(account, amount); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/contracts/src/gateway-strategies/EqualStrategy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "../interfaces/IGatewayStrategy.sol"; 4 | import "../interfaces/IGatewayRegistry.sol"; 5 | import "../interfaces/IRouter.sol"; 6 | 7 | contract EqualStrategy is IGatewayStrategy { 8 | IRouter public router; 9 | IGatewayRegistry public gatewayRegistry; 10 | 11 | constructor(IRouter _router, IGatewayRegistry _gatewayRegistry) { 12 | router = _router; 13 | gatewayRegistry = _gatewayRegistry; 14 | } 15 | 16 | function computationUnitsPerEpoch(bytes calldata gatewayId, uint256) external view returns (uint256) { 17 | return gatewayRegistry.computationUnitsAvailable(gatewayId) / router.workerRegistration().getActiveWorkerCount(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/src/gateway-strategies/SubequalStrategy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "../interfaces/IGatewayStrategy.sol"; 4 | import "../interfaces/IGatewayRegistry.sol"; 5 | import "../interfaces/IRouter.sol"; 6 | 7 | contract SubequalStrategy is IGatewayStrategy { 8 | IRouter public router; 9 | IGatewayRegistry public gatewayRegistry; 10 | 11 | mapping(address gatewayOperator => mapping(uint256 workerId => bool)) public isWorkerSupported; 12 | mapping(address gatewayOperator => uint256) public workerCount; 13 | 14 | event WorkerSupported(address gatewayOperator, uint256 workerId); 15 | event WorkerUnsupported(address gatewayOperator, uint256 workerId); 16 | 17 | constructor(IRouter _router, IGatewayRegistry _gatewayRegistry) { 18 | router = _router; 19 | gatewayRegistry = _gatewayRegistry; 20 | } 21 | 22 | function supportWorkers(uint256[] calldata workerIds) external { 23 | for (uint256 i = 0; i < workerIds.length; i++) { 24 | isWorkerSupported[msg.sender][workerIds[i]] = true; 25 | emit WorkerSupported(msg.sender, workerIds[i]); 26 | } 27 | workerCount[msg.sender] += workerIds.length; 28 | } 29 | 30 | function unsupportWorkers(uint256[] calldata workerIds) external { 31 | for (uint256 i = 0; i < workerIds.length; i++) { 32 | isWorkerSupported[msg.sender][workerIds[i]] = false; 33 | emit WorkerUnsupported(msg.sender, workerIds[i]); 34 | } 35 | workerCount[msg.sender] -= workerIds.length; 36 | } 37 | 38 | function computationUnitsPerEpoch(bytes calldata gatewayId, uint256 workerId) external view returns (uint256) { 39 | address operator = gatewayRegistry.getGateway(gatewayId).operator; 40 | if (!isWorkerSupported[operator][workerId]) { 41 | return 0; 42 | } 43 | return gatewayRegistry.computationUnitsAvailable(gatewayId) / workerCount[operator]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IERC20WithMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 6 | 7 | interface IERC20WithMetadata is IERC20, IERC20Metadata {} 8 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IGatewayRegistry.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | interface IGatewayRegistry { 4 | struct Stake { 5 | uint256 amount; 6 | uint128 lockStart; 7 | uint128 lockEnd; 8 | uint128 duration; 9 | bool autoExtension; 10 | uint256 oldCUs; 11 | } 12 | 13 | struct Gateway { 14 | address operator; 15 | address ownAddress; 16 | bytes peerId; 17 | string metadata; 18 | } 19 | 20 | event Registered(address indexed gatewayOperator, bytes32 indexed id, bytes peerId); 21 | event Staked( 22 | address indexed gatewayOperator, uint256 amount, uint128 lockStart, uint128 lockEnd, uint256 computationUnits 23 | ); 24 | event Unstaked(address indexed gatewayOperator, uint256 amount); 25 | event Unregistered(address indexed gatewayOperator, bytes peerId); 26 | 27 | event AllocatedCUs(address indexed gateway, bytes peerId, uint256[] workerIds, uint256[] shares); 28 | 29 | event StrategyAllowed(address indexed strategy, bool isAllowed); 30 | event DefaultStrategyChanged(address indexed strategy); 31 | event ManaChanged(uint256 newCuPerSQD); 32 | event MaxGatewaysPerClusterChanged(uint256 newAmount); 33 | event MinStakeChanged(uint256 newAmount); 34 | 35 | event MetadataChanged(address indexed gatewayOperator, bytes peerId, string metadata); 36 | event GatewayAddressChanged(address indexed gatewayOperator, bytes peerId, address newAddress); 37 | event UsedStrategyChanged(address indexed gatewayOperator, address strategy); 38 | event AutoextensionEnabled(address indexed gatewayOperator); 39 | event AutoextensionDisabled(address indexed gatewayOperator, uint128 lockEnd); 40 | 41 | event AverageBlockTimeChanged(uint256 newBlockTime); 42 | 43 | function computationUnitsAvailable(bytes calldata gateway) external view returns (uint256); 44 | function getUsedStrategy(bytes calldata peerId) external view returns (address); 45 | function getActiveGateways(uint256 pageNumber, uint256 perPage) external view returns (bytes[] memory); 46 | function getGateway(bytes calldata peerId) external view returns (Gateway memory); 47 | function getActiveGatewaysCount() external view returns (uint256); 48 | } 49 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IGatewayStrategy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | interface IGatewayStrategy { 4 | function computationUnitsPerEpoch(bytes calldata gatewayId, uint256 workerId) external view returns (uint256); 5 | } 6 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/INetworkController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | interface INetworkController { 5 | /// @dev Emitted when epoch length is updated 6 | event EpochLengthUpdated(uint128 epochLength); 7 | /// @dev Emitted when bond amount is updated 8 | event BondAmountUpdated(uint256 bondAmount); 9 | /// @dev Emitted when storage per worker is updated 10 | event StoragePerWorkerInGbUpdated(uint128 storagePerWorkerInGb); 11 | event StakingDeadlockUpdated(uint256 stakingDeadlock); 12 | event AllowedVestedTargetUpdated(address target, bool isAllowed); 13 | event TargetCapacityUpdated(uint256 target); 14 | event RewardCoefficientUpdated(uint256 coefficient); 15 | event LockPeriodUpdated(uint256 lockPeriod); 16 | 17 | /// @notice Deprecated 18 | /// @dev Amount of blocks in one epoch 19 | /// @notice It's now lock period for workers for compatibility reason 20 | function epochLength() external view returns (uint128); 21 | 22 | /// @dev Amount of blocks in one epoch 23 | function workerEpochLength() external view returns (uint128); 24 | 25 | /// @dev Amount of tokens required to register a worker 26 | function bondAmount() external view returns (uint256); 27 | 28 | /// @dev Block when next epoch starts 29 | function nextEpoch() external view returns (uint128); 30 | 31 | /// @dev Number of current epoch (starting from 0 when contract is deployed) 32 | function epochNumber() external view returns (uint128); 33 | 34 | /// @dev Number of unrewarded epochs after which staking will be blocked 35 | function stakingDeadlock() external view returns (uint256); 36 | 37 | /// @dev Number of current epoch (starting from 0 when contract is deployed) 38 | function targetCapacityGb() external view returns (uint256); 39 | 40 | /// @dev Amount of storage in GB each worker is expected to provide 41 | function storagePerWorkerInGb() external view returns (uint128); 42 | 43 | /// @dev Can the `target` be used as a called by the vesting contract 44 | function isAllowedVestedTarget(address target) external view returns (bool); 45 | 46 | /// @dev Max part of initial reward pool that can be allocated during a year, in basis points 47 | /// example: 3000 will mean that on each epoch, max 30% of the initial pool * epoch length / 1 year can be allocated 48 | function yearlyRewardCapCoefficient() external view returns (uint256); 49 | } 50 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IRewardCalculation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | interface IRewardCalculation { 5 | function currentApy() external view returns (uint256); 6 | 7 | function boostFactor(uint256 duration) external pure returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IRewardsDistribution.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | interface IRewardsDistribution { 5 | /// @dev Emitted when rewards are claimed 6 | event Claimed(address indexed by, uint256 indexed worker, uint256 amount); 7 | 8 | /// @dev claim rewards for worker 9 | function claim(address worker) external returns (uint256 reward); 10 | 11 | /// @dev get currently claimable rewards for worker 12 | function claimable(address worker) external view returns (uint256 reward); 13 | } 14 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./IWorkerRegistration.sol"; 5 | import "./IStaking.sol"; 6 | import "./INetworkController.sol"; 7 | import "./IRewardCalculation.sol"; 8 | 9 | interface IRouter { 10 | function workerRegistration() external view returns (IWorkerRegistration); 11 | function staking() external view returns (IStaking); 12 | function rewardTreasury() external view returns (address); 13 | function networkController() external view returns (INetworkController); 14 | function rewardCalculation() external view returns (IRewardCalculation); 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | interface IStaking { 5 | struct StakerRewards { 6 | /// @dev the sum of (amount_i / totalStaked_i) for each distribution of amount_i when totalStaked_i was staked 7 | uint256 cumulatedRewardsPerShare; 8 | /// @dev the value of cumulatedRewardsPerShare when the user's last action was performed (deposit or withdraw) 9 | mapping(address staker => uint256) checkpoint; 10 | /// @dev the amount of tokens staked by the user 11 | mapping(address staker => uint256) depositAmount; 12 | /// @dev block from which withdraw is allowed for staker 13 | mapping(address staker => uint128) withdrawAllowed; 14 | /// @dev the total amount of tokens staked 15 | uint256 totalStaked; 16 | } 17 | 18 | /// @dev Emitted when rewards where distributed by the distributor 19 | event Distributed(uint256 epoch); 20 | /// @dev Emitted when a staker delegates amount to the worker 21 | event Deposited(uint256 indexed worker, address indexed staker, uint256 amount); 22 | /// @dev Emitted when a staker undelegates amount to the worker 23 | event Withdrawn(uint256 indexed worker, address indexed staker, uint256 amount); 24 | /// @dev Emitted when new claimable reward arrives 25 | event Rewarded(uint256 indexed workerId, address indexed staker, uint256 amount); 26 | /// @dev Emitted when a staker claims rewards 27 | event Claimed(address indexed staker, uint256 amount, uint256[] workerIds); 28 | /// @dev Emitted when max delegations is changed 29 | event EpochsLockChanged(uint128 epochsLock); 30 | 31 | event MaxDelegationsChanged(uint256 maxDelegations); 32 | 33 | /// @dev Deposit amount of tokens in favour of a worker 34 | /// @param worker workerId in WorkerRegistration contract 35 | /// @param amount amount of tokens to deposit 36 | function deposit(uint256 worker, uint256 amount) external; 37 | 38 | /// @dev Withdraw amount of tokens staked in favour of a worker 39 | /// @param worker workerId in WorkerRegistration contract 40 | /// @param amount amount of tokens to withdraw 41 | function withdraw(uint256 worker, uint256 amount) external; 42 | 43 | /// @dev Claim rewards for a staker 44 | /// @return amount of tokens claimed 45 | function claim(address staker) external returns (uint256); 46 | 47 | /// @return claimable amount 48 | /// MUST return same value as claim(address staker) but without modifying state 49 | function claimable(address staker) external view returns (uint256); 50 | 51 | /// @dev total staked amount for the worker 52 | function delegated(uint256 worker) external view returns (uint256); 53 | 54 | /// @dev Distribute tokens to stakers in favour of a worker 55 | /// @param workers array of workerIds in WorkerRegistration contract 56 | /// @param amounts array of amounts of tokens to distribute for i-th worker 57 | function distribute(uint256[] calldata workers, uint256[] calldata amounts) external; 58 | } 59 | -------------------------------------------------------------------------------- /packages/contracts/src/interfaces/IWorkerRegistration.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | interface IWorkerRegistration { 5 | /// @dev Emitted when a worker is registered 6 | event WorkerRegistered( 7 | uint256 indexed workerId, bytes peerId, address indexed registrar, uint256 registeredAt, string metadata 8 | ); 9 | 10 | /// @dev Emitted when a worker is deregistered 11 | event WorkerDeregistered(uint256 indexed workerId, address indexed account, uint256 deregistedAt); 12 | 13 | /// @dev Emitted when the bond is withdrawn 14 | event WorkerWithdrawn(uint256 indexed workerId, address indexed account); 15 | 16 | /// @dev Emitted when a excessive bond is withdrawn 17 | event ExcessiveBondReturned(uint256 indexed workerId, uint256 amount); 18 | 19 | /// @dev Emitted when metadata is updated 20 | event MetadataUpdated(uint256 indexed workerId, string metadata); 21 | 22 | function register(bytes calldata peerId, string calldata metadata) external; 23 | 24 | /// @return The number of active workers. 25 | function getActiveWorkerCount() external view returns (uint256); 26 | function getActiveWorkerIds() external view returns (uint256[] memory); 27 | 28 | /// @return The ids of all worker created by the owner account 29 | function getOwnedWorkers(address who) external view returns (uint256[] memory); 30 | 31 | function nextWorkerId() external view returns (uint256); 32 | 33 | function isWorkerActive(uint256 workerId) external view returns (bool); 34 | } 35 | -------------------------------------------------------------------------------- /packages/contracts/test/BaseTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 5 | import "forge-std/Test.sol"; 6 | import "forge-std/Vm.sol"; 7 | import "../src/WorkerRegistration.sol"; 8 | import "../src/Staking.sol"; 9 | import "../src/NetworkController.sol"; 10 | import "../src/RewardTreasury.sol"; 11 | import "../src/Router.sol"; 12 | import "../src/SQD.sol"; 13 | import "../src/RewardCalculation.sol"; 14 | import "../src/GatewayRegistry.sol"; 15 | 16 | contract MockRewardsDistribution is IRewardsDistribution { 17 | function claimable(address) external pure override returns (uint256) { 18 | return 69; 19 | } 20 | 21 | function claim(address) external pure override returns (uint256) { 22 | return 69; 23 | } 24 | } 25 | 26 | contract BaseTest is Test { 27 | SoftCap cap; 28 | GatewayRegistry gatewayRegistry; 29 | 30 | function deployAll() internal returns (SQD token, Router router) { 31 | router = Router(address(new TransparentUpgradeableProxy(address(new Router()), address(1234), ""))); 32 | uint256[] memory minted = new uint256[](1); 33 | minted[0] = 1_337_000_000 ether; 34 | address[] memory holders = new address[](1); 35 | holders[0] = address(this); 36 | 37 | token = new SQD(holders, minted, IL1CustomGateway(address(0)), IGatewayRouter2(address(0))); 38 | 39 | IWorkerRegistration workerRegistration = new WorkerRegistration(token, router); 40 | IStaking staking = new Staking(token, router); 41 | RewardTreasury treasury = new RewardTreasury(token); 42 | cap = new SoftCap(router); 43 | RewardCalculation rewards = new RewardCalculation(router, cap); 44 | address[] memory allowedTargets = new address[](3); 45 | allowedTargets[0] = address(workerRegistration); 46 | allowedTargets[1] = address(staking); 47 | allowedTargets[2] = address(treasury); 48 | INetworkController networkController = new NetworkController(5, 0, 0, 10 ether, allowedTargets); 49 | router.initialize(workerRegistration, staking, address(treasury), networkController, rewards); 50 | gatewayRegistry = 51 | GatewayRegistry(address(new TransparentUpgradeableProxy(address(new GatewayRegistry()), address(1234), ""))); 52 | gatewayRegistry.initialize(IERC20WithMetadata(address(token)), router); 53 | } 54 | 55 | function getCaller() internal returns (address) { 56 | (VmSafe.CallerMode mode, address prank,) = vm.readCallers(); 57 | if (mode == VmSafe.CallerMode.Prank || mode == VmSafe.CallerMode.RecurrentPrank) { 58 | return prank; 59 | } 60 | return address(this); 61 | } 62 | 63 | function expectNotAdminRevert() internal { 64 | vm.expectRevert( 65 | abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, getCaller(), bytes32(0)) 66 | ); 67 | } 68 | 69 | function expectNotRoleRevert(bytes32 role) internal { 70 | vm.expectRevert(abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, getCaller(), role)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/contracts/test/DistributedRewardsDistribution/DistributedRewardsDistribution.addAndRemoveDistributors.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./DistributedRewardsDistribution.sol"; 4 | 5 | contract RewardsDistributionAddRemoveDistributorsTest is RewardsDistributionTest { 6 | function test_RevertsIf_NonAdminAddsDistributor() public { 7 | hoax(address(1)); 8 | expectNotAdminRevert(); 9 | rewardsDistribution.addDistributor(address(1)); 10 | } 11 | 12 | function test_RevertsIf_NonAdminRemovesDistributor() public { 13 | rewardsDistribution.addDistributor(address(1)); 14 | hoax(address(1)); 15 | expectNotAdminRevert(); 16 | rewardsDistribution.removeDistributor(address(1)); 17 | } 18 | 19 | function test_RevertsIf_AddingSameDistributorTwice() public { 20 | rewardsDistribution.addDistributor(address(1)); 21 | vm.expectRevert("Distributor already added"); 22 | rewardsDistribution.addDistributor(address(1)); 23 | } 24 | 25 | function test_RevertsIf_RemovingUnknownDistributor() public { 26 | rewardsDistribution.addDistributor(address(1)); 27 | vm.expectRevert("Distributor does not exist"); 28 | rewardsDistribution.removeDistributor(address(2)); 29 | } 30 | 31 | function test_RevertsIf_AmountOfDistributorsBelowRequiredApproves() public { 32 | rewardsDistribution.addDistributor(address(1)); 33 | rewardsDistribution.setApprovesRequired(2); 34 | vm.expectRevert("Not enough distributors to approve distribution"); 35 | rewardsDistribution.removeDistributor(address(1)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/contracts/test/DistributedRewardsDistribution/DistributedRewardsDistribution.claim.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./DistributedRewardsDistribution.sol"; 4 | 5 | contract RewardsDistributionClaimTest is RewardsDistributionTest { 6 | function testTransfersClaimableRewardsToSender() public { 7 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(4); 8 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 9 | uint256 claimable = rewardsDistribution.claimable(workerOwner); 10 | uint256 balanceBefore = token.balanceOf(workerOwner); 11 | hoax(workerOwner); 12 | treasury.claim(rewardsDistribution); 13 | assertEq(rewardsDistribution.claimable(workerOwner), 0); 14 | assertEq(token.balanceOf(workerOwner) - balanceBefore, claimable); 15 | } 16 | 17 | function testCannotClaimSameRewardTwice() public { 18 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(4); 19 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 20 | uint256 claimable = rewardsDistribution.claimable(workerOwner); 21 | uint256 balanceBefore = token.balanceOf(workerOwner); 22 | hoax(workerOwner); 23 | treasury.claim(rewardsDistribution); 24 | assertEq(token.balanceOf(workerOwner) - balanceBefore, claimable); 25 | 26 | hoax(workerOwner); 27 | treasury.claim(rewardsDistribution); 28 | assertEq(token.balanceOf(workerOwner) - balanceBefore, claimable); 29 | } 30 | 31 | function testClaimEmitsEvent() public { 32 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(4); 33 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 34 | uint256 claimable = rewardsDistribution.claimable(workerOwner); 35 | hoax(workerOwner); 36 | vm.expectEmit(address(rewardsDistribution)); 37 | emit Claimed(workerOwner, 1, claimable); 38 | treasury.claim(rewardsDistribution); 39 | } 40 | 41 | function testDistributorClaimCannotBeCalledByNotTreasury() public { 42 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(1); 43 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 44 | expectNotRoleRevert(rewardsDistribution.REWARDS_TREASURY_ROLE()); 45 | rewardsDistribution.claim(workerOwner); 46 | } 47 | 48 | function test_CanClaimRewardsForWithdrawnWorker() public { 49 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(1); 50 | startHoax(workerOwner); 51 | vm.roll(block.number + 5); 52 | workerRegistration.deregister(workerId); 53 | vm.roll(block.number + 4); 54 | workerRegistration.withdraw(workerId); 55 | startHoax(address(this)); 56 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 57 | uint256 claimable = rewardsDistribution.claimable(workerOwner); 58 | assertGt(claimable, 0); 59 | uint256 balanceBefore = token.balanceOf(workerOwner); 60 | startHoax(workerOwner); 61 | treasury.claim(rewardsDistribution); 62 | assertEq(token.balanceOf(workerOwner) - balanceBefore, claimable); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/contracts/test/DistributedRewardsDistribution/DistributedRewardsDistribution.distribute.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./DistributedRewardsDistribution.sol"; 4 | 5 | contract RewardsDistributionDistributeTest is RewardsDistributionTest { 6 | function gasUsageForNWorkers(uint256 n) internal { 7 | staking.setMaxDelegations(1000); 8 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(n); 9 | uint256 gasBefore = gasleft(); 10 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 11 | uint256 gasAfter = gasleft(); 12 | uint256 gasUsed = gasBefore - gasAfter; 13 | emit log_named_uint("gasUsed", gasUsed); 14 | } 15 | 16 | function testDistributeGasUsageFor10Workers() public { 17 | gasUsageForNWorkers(10); 18 | } 19 | 20 | function testDistributeGasUsageFor100Workers() public { 21 | gasUsageForNWorkers(100); 22 | } 23 | 24 | function testDistributeGasUsageFor1000Workers() public { 25 | gasUsageForNWorkers(1000); 26 | } 27 | 28 | function test_RevertsIf_SomeBlocksSkipped() public { 29 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(2); 30 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 31 | vm.expectRevert("Not all blocks covered"); 32 | rewardsDistribution.distributeHelper(4, recipients, workerAmounts, stakerAmounts); 33 | } 34 | 35 | function testIncreasesClaimableAmount() public { 36 | (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) = prepareRewards(1); 37 | rewardsDistribution.distributeHelper(1, recipients, workerAmounts, stakerAmounts); 38 | assertEq(rewardsDistribution.claimable(workerOwner), epochRewardAmount); 39 | rewardsDistribution.distributeHelper(3, recipients, workerAmounts, stakerAmounts); 40 | assertEq(rewardsDistribution.claimable(workerOwner), epochRewardAmount * 2); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/contracts/test/DistributedRewardsDistribution/DistributedRewardsDistribution.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "../../src/DistributedRewardDistribution.sol"; 5 | import "../../src/SQD.sol"; 6 | import "../../src/RewardTreasury.sol"; 7 | import "../../src/NetworkController.sol"; 8 | import "../../src/Staking.sol"; 9 | import "../../src/WorkerRegistration.sol"; 10 | import "../BaseTest.sol"; 11 | 12 | contract DistributionHelper is DistributedRewardsDistribution { 13 | constructor(IRouter router) DistributedRewardsDistribution(router) {} 14 | 15 | function distributeHelper( 16 | uint256 fromBlock, 17 | uint256[] calldata recipients, 18 | uint256[] calldata workerRewards, 19 | uint256[] calldata _stakerRewards 20 | ) public { 21 | distribute(fromBlock, fromBlock + 1, recipients, workerRewards, _stakerRewards); 22 | } 23 | } 24 | 25 | contract RewardsDistributionTest is BaseTest { 26 | bytes workerId = "1337"; 27 | uint256 epochRewardAmount = 1000; 28 | address workerOwner = address(1); 29 | DistributionHelper rewardsDistribution; 30 | RewardTreasury treasury; 31 | Staking staking; 32 | WorkerRegistration workerRegistration; 33 | IERC20 token; 34 | 35 | event Claimed(address indexed who, uint256 indexed workerId, uint256 amount); 36 | 37 | function setUp() public { 38 | (SQD _token, Router router) = deployAll(); 39 | token = _token; 40 | staking = Staking(address(router.staking())); 41 | workerRegistration = WorkerRegistration(address(router.workerRegistration())); 42 | treasury = RewardTreasury(router.rewardTreasury()); 43 | token.transfer(workerOwner, token.totalSupply() / 2); 44 | NetworkController(address(router.networkController())).setEpochLength(2); 45 | token.approve(address(staking), type(uint256).max); 46 | hoax(workerOwner); 47 | token.approve(address(workerRegistration), type(uint256).max); 48 | hoax(workerOwner); 49 | workerRegistration.register(workerId); 50 | vm.mockCall( 51 | address(workerRegistration), abi.encodeWithSelector(WorkerRegistration.isWorkerActive.selector), abi.encode(true) 52 | ); 53 | rewardsDistribution = new DistributionHelper(router); 54 | staking.grantRole(staking.REWARDS_DISTRIBUTOR_ROLE(), address(rewardsDistribution)); 55 | rewardsDistribution.addDistributor(address(this)); 56 | rewardsDistribution.grantRole(rewardsDistribution.REWARDS_TREASURY_ROLE(), address(treasury)); 57 | treasury.setWhitelistedDistributor(rewardsDistribution, true); 58 | token.transfer(address(treasury), epochRewardAmount * 10); 59 | } 60 | 61 | function prepareRewards(uint256 n) 62 | internal 63 | returns (uint256[] memory recipients, uint256[] memory workerAmounts, uint256[] memory stakerAmounts) 64 | { 65 | workerAmounts = new uint256[](n); 66 | stakerAmounts = new uint256[](n); 67 | recipients = new uint256[](n); 68 | for (uint160 i = 0; i < n; i++) { 69 | staking.deposit(i + 1, 1); 70 | workerAmounts[i] = epochRewardAmount / n; 71 | stakerAmounts[i] = 1; 72 | recipients[i] = i + 1; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.allocate.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./GatewayRegistryTest.sol"; 4 | 5 | contract GatewayRegistryAllocateTest is GatewayRegistryTest { 6 | function gasUsageForNWorkers(uint256 n) internal { 7 | uint256[] memory workerIds = new uint256[](n); 8 | uint256[] memory cus = new uint256[](n); 9 | for (uint256 i = 0; i < n; i++) { 10 | workerIds[i] = i + 1; 11 | cus[i] = 10; 12 | } 13 | 14 | vm.mockCall( 15 | address(gatewayRegistry.router().workerRegistration()), 16 | abi.encodeWithSelector(WorkerRegistration.nextWorkerId.selector), 17 | abi.encode(100000) 18 | ); 19 | uint256 gasBefore = gasleft(); 20 | gatewayRegistry.allocateComputationUnits(workerIds, cus); 21 | uint256 gasAfter = gasleft(); 22 | uint256 gasUsed = gasBefore - gasAfter; 23 | emit log_named_uint("gasUsed", gasUsed); 24 | } 25 | 26 | function test_AllocateCUsGasUsageFor1000Workers() public { 27 | gatewayRegistry.stake(10000 ether, 2000); 28 | gasUsageForNWorkers(1000); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.allocatedCUs.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "forge-std/Test.sol"; 4 | import "./GatewayRegistryTest.sol"; 5 | 6 | contract GatewayRegistryAllocatedCUTest is GatewayRegistryTest { 7 | function expectedCUs(uint256 amount, uint256 duration) public view returns (uint256) { 8 | return uint256(amount * duration * rewardCalc.boostFactor(duration * 12) / 10000); 9 | } 10 | 11 | function test_availableCUs() public { 12 | assertEq(gatewayRegistry.computationUnitsAmount(100 ether, 18000), expectedCUs(100, 18000)); 13 | assertEq(gatewayRegistry.computationUnitsAmount(200 ether, 14500), expectedCUs(200, 14500)); 14 | assertEq(gatewayRegistry.computationUnitsAmount(400 ether, 14000), expectedCUs(400, 14000)); 15 | } 16 | 17 | function test_LockShorterThanEpochNotGreaterThanTotalCUAmount() public { 18 | gatewayRegistry.stake(10 ether, 5, true); 19 | NetworkController(address(router.networkController())).setEpochLength(150); 20 | goToNextEpoch(); 21 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 50); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.autoextend.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "forge-std/Test.sol"; 4 | import "./GatewayRegistryTest.sol"; 5 | 6 | contract GatewayRegistryAutoExtensionTest is GatewayRegistryTest { 7 | function test_stakeNeverExpiresWhenAutoextendIsOn() public { 8 | gatewayRegistry.stake(100 ether, 100, true); 9 | goToNextEpoch(); 10 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 500); 11 | vm.roll(block.number + 10000); 12 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 500); 13 | gatewayRegistry.disableAutoExtension(); 14 | vm.roll(block.number + 100); 15 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 0); 16 | } 17 | 18 | function test_disablingAutoextendWillUnlockStakeAtNextUnlockPeriod() public { 19 | gatewayRegistry.stake(100 ether, 100, true); 20 | goToNextEpoch(); 21 | vm.roll(block.number + 10000); 22 | vm.roll(block.number + 33); 23 | gatewayRegistry.disableAutoExtension(); 24 | uint128 lockStart = gatewayRegistry.getStake(address(this)).lockStart; 25 | uint128 lockEnd = gatewayRegistry.getStake(address(this)).lockEnd; 26 | assertEq(lockEnd - block.number, 67); 27 | assertEq((lockEnd - lockStart) % 100, 0); 28 | } 29 | 30 | function test_CanEnableAndDisableAutoextendForNotStartedStakes() public { 31 | gatewayRegistry.stake(100 ether, 100, true); 32 | gatewayRegistry.disableAutoExtension(); 33 | uint128 lockStart = gatewayRegistry.getStake(address(this)).lockStart; 34 | uint128 lockEnd = gatewayRegistry.getStake(address(this)).lockEnd; 35 | assertEq(lockEnd - lockStart, 100); 36 | gatewayRegistry.enableAutoExtension(); 37 | 38 | goToNextEpoch(); 39 | gatewayRegistry.disableAutoExtension(); 40 | lockStart = gatewayRegistry.getStake(address(this)).lockStart; 41 | lockEnd = gatewayRegistry.getStake(address(this)).lockEnd; 42 | assertEq(lockEnd - lockStart, 100); 43 | gatewayRegistry.enableAutoExtension(); 44 | 45 | vm.roll(block.number + 1); 46 | gatewayRegistry.disableAutoExtension(); 47 | lockStart = gatewayRegistry.getStake(address(this)).lockStart; 48 | lockEnd = gatewayRegistry.getStake(address(this)).lockEnd; 49 | assertEq(lockEnd - lockStart, 100); 50 | gatewayRegistry.enableAutoExtension(); 51 | 52 | vm.roll(lockStart + 99); 53 | gatewayRegistry.disableAutoExtension(); 54 | lockStart = gatewayRegistry.getStake(address(this)).lockStart; 55 | lockEnd = gatewayRegistry.getStake(address(this)).lockEnd; 56 | assertEq(lockEnd - lockStart, 100); 57 | gatewayRegistry.enableAutoExtension(); 58 | 59 | vm.roll(block.number + 1); 60 | gatewayRegistry.disableAutoExtension(); 61 | lockStart = gatewayRegistry.getStake(address(this)).lockStart; 62 | lockEnd = gatewayRegistry.getStake(address(this)).lockEnd; 63 | assertEq(lockEnd - lockStart, 200); 64 | gatewayRegistry.enableAutoExtension(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.clusters.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./GatewayRegistryTest.sol"; 4 | 5 | contract GatewayRegistryStakeTest is GatewayRegistryTest { 6 | function compareCluster(bytes memory peerId, bytes[] memory expected) internal { 7 | bytes[] memory cluster = gatewayRegistry.getCluster(peerId); 8 | assertEq(cluster.length, expected.length, "Length not equal"); 9 | for (uint256 i = 0; i < cluster.length; i++) { 10 | assertEq(cluster[i], expected[i]); 11 | } 12 | } 13 | 14 | /// Kinda lazy way to avoid merging arrays 15 | function compareCluster(bytes memory peerId, bytes memory expectedPrefix, bytes[] memory expected) internal { 16 | bytes[] memory cluster = gatewayRegistry.getCluster(peerId); 17 | assertEq(cluster.length - 1, expected.length, "Length not equal"); 18 | assertEq(cluster[0], expectedPrefix); 19 | for (uint256 i = 1; i < cluster.length; i++) { 20 | assertEq(cluster[i], expected[i - 1]); 21 | } 22 | } 23 | 24 | function test_ClusterReturnsCorrectSetOfGateways() public { 25 | gatewayRegistry.register(myPeers, metadatas, addresses); 26 | hoax(address(2)); 27 | gatewayRegistry.register(notMyPeers, metadatas, addresses); 28 | compareCluster(notMyPeers[0], notMyPeers); 29 | compareCluster(notMyPeers[2], notMyPeers); 30 | compareCluster(myPeers[2], peerId, myPeers); 31 | compareCluster(myPeers[0], peerId, myPeers); 32 | compareCluster(peerId, peerId, myPeers); 33 | gatewayRegistry.unregister(c(peerId, myPeers[2])); 34 | compareCluster(myPeers[0], c(myPeers[1], myPeers[0])); 35 | compareCluster(notMyPeers[2], notMyPeers); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.getActiveGateways.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./GatewayRegistryTest.sol"; 4 | 5 | contract GatewayRegistryActiveGatewaysTest is GatewayRegistryTest { 6 | function test_GetActiveGatewaysPagination() public { 7 | NetworkController(address(router.networkController())).setEpochLength(2); 8 | bytes memory gatewayId = "gatewayId"; 9 | gatewayRegistry.register(abi.encodePacked(gatewayId, "3")); 10 | gatewayRegistry.register(abi.encodePacked(gatewayId, "4")); 11 | gatewayRegistry.register(abi.encodePacked(gatewayId, "5")); 12 | gatewayRegistry.register(abi.encodePacked(gatewayId, "6")); 13 | gatewayRegistry.register(abi.encodePacked(gatewayId, "7")); 14 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 0); 15 | assertEq(gatewayRegistry.getActiveGateways(0, 100).length, 0); 16 | gatewayRegistry.stake(1, 10); 17 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 6); 18 | assertEq(gatewayRegistry.getActiveGateways(0, 100).length, 6); 19 | 20 | assertEq(gatewayRegistry.getActiveGateways(1, 2).length, 2); 21 | assertEq(gatewayRegistry.getActiveGateways(1, 2)[0], abi.encodePacked(gatewayId, "4")); 22 | assertEq(gatewayRegistry.getActiveGateways(1, 2)[1], abi.encodePacked(gatewayId, "5")); 23 | 24 | assertEq(gatewayRegistry.getActiveGateways(1, 4).length, 2); 25 | assertEq(gatewayRegistry.getActiveGateways(1, 4)[0], abi.encodePacked(gatewayId, "6")); 26 | assertEq(gatewayRegistry.getActiveGateways(1, 4)[1], abi.encodePacked(gatewayId, "7")); 27 | 28 | assertEq(gatewayRegistry.getActiveGateways(2, 4).length, 0); 29 | } 30 | 31 | function test_NewGatewaysAreAddedToActiveGateways() public { 32 | bytes memory gatewayId = "gatewayId"; 33 | gatewayRegistry.stake(1, 5); 34 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 1); 35 | gatewayRegistry.register(gatewayId); 36 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 2); 37 | } 38 | 39 | function test_WholeClusterIsRemovedFromListAfterUnstake() public { 40 | bytes memory gatewayId = "gatewayId"; 41 | gatewayRegistry.stake(1, 5); 42 | gatewayRegistry.register(gatewayId); 43 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 2); 44 | vm.roll(block.number + 10); 45 | gatewayRegistry.unstake(); 46 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 0); 47 | } 48 | 49 | function test_GatewayRemovedAfterUnregister() public { 50 | bytes memory gatewayId = "gatewayId"; 51 | gatewayRegistry.stake(1, 5); 52 | gatewayRegistry.register(gatewayId); 53 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 2); 54 | gatewayRegistry.unregister(gatewayId); 55 | assertEq(gatewayRegistry.getActiveGatewaysCount(), 1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.registrAndUnregister.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "./GatewayRegistryTest.sol"; 4 | 5 | contract GatewayRegistryRegisterTest is GatewayRegistryTest { 6 | function test_CannotRegisterSamePeerIdTwice() public { 7 | gatewayRegistry.register(myPeers, metadatas, addresses); 8 | startHoax(address(2)); 9 | vm.expectRevert("PeerId already registered"); 10 | gatewayRegistry.register(c(notMyPeers[0], notMyPeers[1], peerId), metadatas, addresses); 11 | vm.expectRevert("PeerId already registered"); 12 | gatewayRegistry.register(c(notMyPeers[0], notMyPeers[1], notMyPeers[0]), metadatas, addresses); 13 | } 14 | 15 | function test_CannotUnregisterNotOwnGateway() public { 16 | hoax(address(2)); 17 | vm.expectRevert("Only operator can call this function"); 18 | gatewayRegistry.unregister(peerId); 19 | } 20 | 21 | function test_DoesNotChangeStrategyAfterFirstRegistration() public { 22 | assertEq(gatewayRegistry.getUsedStrategy(peerId), defaultStrategy); 23 | gatewayRegistry.setIsStrategyAllowed(address(0), true, true); 24 | gatewayRegistry.register(myPeers, metadatas, addresses); 25 | assertEq(gatewayRegistry.getUsedStrategy(peerId), defaultStrategy); 26 | } 27 | 28 | function test_CorrectlySetsMetadataAndAddress() public { 29 | gatewayRegistry.register(myPeers, metadatas, addresses); 30 | assertEq(gatewayRegistry.getMetadata(myPeers[1]), "some test metadata"); 31 | assertEq(gatewayRegistry.gatewayByAddress(addresses[2]), keccak256(myPeers[2])); 32 | assertEq(gatewayRegistry.getGateway(myPeers[2]).ownAddress, addresses[2]); 33 | assertEq(gatewayRegistry.gatewayByAddress(address(0)), bytes32(0)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.stake.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "forge-std/Test.sol"; 4 | import "./GatewayRegistryTest.sol"; 5 | 6 | contract GatewayRegistryStakeTest is GatewayRegistryTest { 7 | function test_StakingTransfersTokensToContract() public { 8 | uint256 balanceBefore = token.balanceOf(address(this)); 9 | gatewayRegistry.stake(100, 200); 10 | assertEq(token.balanceOf(address(this)), balanceBefore - 100); 11 | assertEq(token.balanceOf(address(gatewayRegistry)), 100); 12 | } 13 | 14 | function test_StakingStoresStakedAmountAndUnlockTimestamp() public { 15 | gatewayRegistry.stake(100, 200); 16 | assertStake(0, 100, 205); 17 | goToNextEpoch(); 18 | gatewayRegistry.addStake(1000); 19 | assertStake(0, 1100, 210); 20 | } 21 | 22 | function test_StakingIncreasesStakedAmount() public { 23 | gatewayRegistry.stake(100, 200); 24 | assertEq(gatewayRegistry.staked(address(this)), 100); 25 | goToNextEpoch(); 26 | gatewayRegistry.addStake(1000); 27 | assertEq(gatewayRegistry.staked(address(this)), 1100); 28 | } 29 | 30 | function test_IncreasesComputationalUnits() public { 31 | gatewayRegistry.stake(10 ether, 150_000); 32 | goToNextEpoch(); 33 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 50); 34 | gatewayRegistry.addStake(5 ether); 35 | goToNextEpoch(); 36 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 75); 37 | gatewayRegistry.addStake(1 ether); 38 | goToNextEpoch(); 39 | assertEq(gatewayRegistry.computationUnitsAvailable(peerId), 80); 40 | } 41 | 42 | function test_EmitsEvent() public { 43 | vm.expectEmit(address(gatewayRegistry)); 44 | uint128 nextEpoch = router.networkController().nextEpoch(); 45 | emit Staked(address(this), 100, nextEpoch, nextEpoch + 200, 0); 46 | gatewayRegistry.stake(100, 200); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistry.unstake.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "forge-std/Test.sol"; 4 | import "./GatewayRegistryTest.sol"; 5 | 6 | contract GatewayRegistryUnStakeTest is GatewayRegistryTest { 7 | function test_RevertsIf_UnstakedWithoutStake() public { 8 | vm.expectRevert("Nothing to unstake"); 9 | gatewayRegistry.unstake(); 10 | } 11 | 12 | function test_RevertsIf_TryingToUnstakeLockedAmount() public { 13 | gatewayRegistry.stake(100, 200); 14 | vm.expectRevert("Stake is locked"); 15 | gatewayRegistry.unstake(); 16 | } 17 | 18 | function test_UnstakeDecreasesStakedAmount() public { 19 | gatewayRegistry.stake(100, 200); 20 | vm.roll(block.number + 300); 21 | gatewayRegistry.unstake(); 22 | assertEq(gatewayRegistry.staked(address(this)), 0); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/test/GatewayRegistry/GatewayRegistryTest.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "forge-std/Test.sol"; 4 | import "../../src/GatewayRegistry.sol"; 5 | import "../../src/SQD.sol"; 6 | import "../../src/NetworkController.sol"; 7 | import "../../src/WorkerRegistration.sol"; 8 | import "../../src/Staking.sol"; 9 | import "../BaseTest.sol"; 10 | 11 | contract GatewayRegistryTest is BaseTest { 12 | SQD token; 13 | RewardCalculation rewardCalc; 14 | Router router; 15 | bytes peerId = "peerId"; 16 | address defaultStrategy = address(2137); 17 | 18 | bytes[] myPeers = [bytes("my-peer-1"), "my-peer-2", "my-peer-3"]; 19 | bytes[] notMyPeers = [bytes("some-gateway-1"), "some-gateway-2", "some-gateway-3"]; 20 | string[] metadatas = ["", "some test metadata", ""]; 21 | address[] addresses = [address(0), address(0), address(1)]; 22 | 23 | event Staked(address indexed gateway, uint256 amount, uint128 lockStart, uint128 lockedUntil, uint256 cus); 24 | 25 | function setUp() public { 26 | (token, router) = deployAll(); 27 | rewardCalc = RewardCalculation(address(router.rewardCalculation())); 28 | gatewayRegistry.setIsStrategyAllowed(defaultStrategy, true, true); 29 | token.approve(address(gatewayRegistry), type(uint256).max); 30 | gatewayRegistry.register(peerId, "", address(this)); 31 | } 32 | 33 | function assertStake(uint256, uint256 amount, uint256 lockedUntil) internal { 34 | GatewayRegistry.Stake memory stake = gatewayRegistry.getStake(address(this)); 35 | assertEq(amount, stake.amount); 36 | assertEq(lockedUntil, stake.lockEnd); 37 | } 38 | 39 | function c(bytes memory first) internal pure returns (bytes[] memory) { 40 | bytes[] memory result = new bytes[](1); 41 | result[0] = first; 42 | return result; 43 | } 44 | 45 | function c(bytes memory first, bytes memory second) internal pure returns (bytes[] memory) { 46 | return c(c(first), c(second)); 47 | } 48 | 49 | function c(bytes memory first, bytes memory second, bytes memory third) internal pure returns (bytes[] memory) { 50 | return c(c(first, second), c(third)); 51 | } 52 | 53 | function c(bytes[] memory first, bytes[] memory second) internal pure returns (bytes[] memory) { 54 | bytes[] memory result = new bytes[](first.length + second.length); 55 | for (uint256 i = 0; i < first.length; i++) { 56 | result[i] = first[i]; 57 | } 58 | for (uint256 i = 0; i < second.length; i++) { 59 | result[i + first.length] = second[i]; 60 | } 61 | return result; 62 | } 63 | 64 | function goToNextEpoch() internal { 65 | uint128 nextEpoch = router.networkController().nextEpoch(); 66 | vm.roll(nextEpoch); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/contracts/test/LinearToSqerCap.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./BaseTest.sol"; 5 | import "../src/SoftCap.sol"; 6 | import {LinearToSqrtCap} from "../src/LinearToSqrtCap.sol"; 7 | 8 | contract LinearToSqrtCapTest is BaseTest { 9 | Router router; 10 | LinearToSqrtCap sqrtCap; 11 | 12 | function setUp() public { 13 | (, router) = deployAll(); 14 | sqrtCap = new LinearToSqrtCap(router); 15 | } 16 | 17 | function test_CapedStakeLinear() public { 18 | assertCap(0, 0); 19 | assertCap(1000 ether, 1000 ether); 20 | assertCap(20000 ether, 20000 ether); 21 | } 22 | 23 | function test_CapedStakeSqrt() public { 24 | assertCap(20000 ether + 1, 20000 ether); 25 | assertCap(20000 ether + 3, 20000 ether + 1); 26 | assertCap(20001 ether, 20000499993750156245117); 27 | assertCap(30000 ether, 24494897427831780981972); 28 | assertCap(1_000_000 ether, 141421356237309504880168); // ~140k 29 | } 30 | 31 | function assertCap(uint256 mockStake, uint256 expected) internal { 32 | vm.mockCall(address(router.staking()), abi.encodeWithSelector(IStaking.delegated.selector), abi.encode(mockStake)); 33 | assertEq(sqrtCap.capedStake(0), expected); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/contracts/test/NetworkController.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "../src/NetworkController.sol"; 5 | import "./BaseTest.sol"; 6 | 7 | contract NetworkControllerTest is BaseTest { 8 | NetworkController controller; 9 | 10 | function setUp() public { 11 | controller = new NetworkController(5, 0, 0, 100 ether, new address[](0)); 12 | } 13 | 14 | function test_NextEpoch() public { 15 | assertEq(controller.nextEpoch(), 5); 16 | vm.roll(4); 17 | assertEq(controller.nextEpoch(), 5); 18 | vm.roll(5); 19 | assertEq(controller.nextEpoch(), 10); 20 | } 21 | 22 | function test_EpochNumber() public { 23 | assertEq(controller.epochNumber(), 0); 24 | vm.roll(4); 25 | assertEq(controller.epochNumber(), 0); 26 | vm.roll(5); 27 | assertEq(controller.epochNumber(), 1); 28 | vm.roll(14); 29 | assertEq(controller.epochNumber(), 2); 30 | vm.roll(15); 31 | assertEq(controller.epochNumber(), 3); 32 | } 33 | 34 | function test_EpochNumberAfterEpochLengthChange() public { 35 | vm.roll(27); 36 | assertEq(controller.epochNumber(), 5); 37 | assertEq(controller.nextEpoch(), 30); 38 | controller.setEpochLength(13); 39 | assertEq(controller.nextEpoch(), 30); 40 | assertEq(controller.epochNumber(), 5); 41 | vm.roll(29); 42 | assertEq(controller.epochNumber(), 5); 43 | vm.roll(30); 44 | assertEq(controller.epochNumber(), 6); 45 | assertEq(controller.nextEpoch(), 43); 46 | vm.roll(42); 47 | assertEq(controller.epochNumber(), 6); 48 | vm.roll(43); 49 | assertEq(controller.epochNumber(), 7); 50 | assertEq(controller.nextEpoch(), 56); 51 | } 52 | 53 | function test_RevertsIf_SettingEpochLengthNotByAdmin() public { 54 | hoax(address(1)); 55 | expectNotAdminRevert(); 56 | controller.setEpochLength(10); 57 | } 58 | 59 | function test_RevertsIf_SettingEpochLengthTo1() public { 60 | vm.expectRevert("Epoch length too short"); 61 | controller.setEpochLength(1); 62 | } 63 | 64 | function test_RevertsIf_SettingBondAmountNotByAdmin() public { 65 | hoax(address(1)); 66 | expectNotAdminRevert(); 67 | controller.setBondAmount(10); 68 | } 69 | 70 | function test_RevertsIf_SettingBondAmountTo0() public { 71 | vm.expectRevert("Bond cannot be 0"); 72 | controller.setBondAmount(0); 73 | } 74 | 75 | function test_RevertsIf_SettingBondAmountToOver1M() public { 76 | vm.expectRevert("Bond too large"); 77 | controller.setBondAmount(1_000_001 ether); 78 | } 79 | 80 | function test_RevertsIf_SettingStorageAmountNotByAdmin() public { 81 | hoax(address(1)); 82 | expectNotAdminRevert(); 83 | controller.setStoragePerWorkerInGb(10); 84 | } 85 | 86 | function test_RevertsIf_SettingStorageAmountTo0() public { 87 | vm.expectRevert("Storage cannot be 0"); 88 | controller.setStoragePerWorkerInGb(0); 89 | } 90 | 91 | function test_changesBondAmount() public { 92 | assertEq(controller.bondAmount(), 100 ether); 93 | controller.setBondAmount(10); 94 | assertEq(controller.bondAmount(), 10); 95 | } 96 | 97 | function test_changesStorageAmount() public { 98 | assertEq(controller.storagePerWorkerInGb(), 1000); 99 | controller.setStoragePerWorkerInGb(10); 100 | assertEq(controller.storagePerWorkerInGb(), 10); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/contracts/test/RewardTreasury.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "../src/RewardTreasury.sol"; 5 | import "../src/SQD.sol"; 6 | import "./BaseTest.sol"; 7 | 8 | contract RewardTreasuryTest is BaseTest { 9 | RewardTreasury treasury; 10 | SQD token; 11 | IRewardsDistribution distributor; 12 | 13 | event Claimed(address indexed by, address indexed receiver, uint256 amount); 14 | event WhitelistedDistributorSet(IRewardsDistribution indexed distributor, bool isWhitelisted); 15 | 16 | function setUp() public { 17 | (SQD _token, Router router) = deployAll(); 18 | token = _token; 19 | treasury = RewardTreasury(router.rewardTreasury()); 20 | token.transfer(address(treasury), 100); 21 | 22 | distributor = new MockRewardsDistribution(); 23 | treasury.setWhitelistedDistributor(distributor, true); 24 | } 25 | 26 | function test_RevertsIf_claimForNotWhitelistedDistributor() public { 27 | vm.expectRevert("Distributor not whitelisted"); 28 | treasury.claim(IRewardsDistribution(address(1))); 29 | } 30 | 31 | function test_ClaimTransfersAmountReturnedByDistributorToSender() public { 32 | uint256 addressBefore = token.balanceOf(address(this)); 33 | treasury.claim(distributor); 34 | uint256 addressAfter = token.balanceOf(address(this)); 35 | assertEq(addressAfter, addressBefore + 69); 36 | } 37 | 38 | function test_ClaimForTransfersAmountReturnedByDistributorToReceiver() public { 39 | treasury.claimFor(distributor, address(2)); 40 | assertEq(token.balanceOf(address(2)), 69); 41 | } 42 | 43 | function test_ClaimEmitsEvent() public { 44 | vm.expectEmit(address(treasury)); 45 | emit Claimed(address(this), address(this), 69); 46 | treasury.claim(distributor); 47 | } 48 | 49 | function test_ClaimForEmitsEvent() public { 50 | vm.expectEmit(address(treasury)); 51 | emit Claimed(address(this), address(2), 69); 52 | treasury.claimFor(distributor, address(2)); 53 | } 54 | 55 | function test_ClaimableReturnsAmountReturnedByDistributor() public { 56 | assertEq(treasury.claimable(distributor, address(1)), 69); 57 | } 58 | 59 | function test_SetWhitelistedDistributorSetsDistributor() public { 60 | treasury.setWhitelistedDistributor(IRewardsDistribution(address(3)), true); 61 | assertEq(treasury.isWhitelistedDistributor(IRewardsDistribution(address(3))), true); 62 | } 63 | 64 | function test_SetWhitelistedDistributorUnsetsDistributor() public { 65 | treasury.setWhitelistedDistributor(distributor, false); 66 | assertEq(treasury.isWhitelistedDistributor(distributor), false); 67 | } 68 | 69 | function test_SetWhitelistedDistributorEmitsEvent() public { 70 | vm.expectEmit(address(treasury)); 71 | emit WhitelistedDistributorSet(distributor, true); 72 | treasury.setWhitelistedDistributor(distributor, true); 73 | } 74 | 75 | function test_RevertsIf_SetWhitelistedDistributorNotCalledByAdmin() public { 76 | hoax(address(2)); 77 | expectNotAdminRevert(); 78 | treasury.setWhitelistedDistributor(distributor, true); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/contracts/test/Staking/StakersRewardDistributor.accessControl.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./StakersRewardDistributorTest.sol"; 5 | 6 | contract StakingAccessControlTest is StakersRewardDistributionTest { 7 | function test_RevertsIf_NotRewardsDistributorCallDistribute() public { 8 | uint256[] memory amounts = new uint256[](1); 9 | amounts[0] = 100; 10 | bytes32 role = staking.REWARDS_DISTRIBUTOR_ROLE(); 11 | hoax(address(1)); 12 | expectNotRoleRevert(role); 13 | staking.distribute(workers, amounts); 14 | } 15 | 16 | function test_RevertsIf_NotRewardsDistributorCallClaim() public { 17 | bytes32 role = staking.REWARDS_DISTRIBUTOR_ROLE(); 18 | hoax(address(1)); 19 | expectNotRoleRevert(role); 20 | staking.claim(address(this)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/contracts/test/Staking/StakersRewardDistributorTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "../../src/Staking.sol"; 6 | import "../../src/SQD.sol"; 7 | import "../BaseTest.sol"; 8 | 9 | contract StakingHelper is Staking { 10 | constructor(IERC20 token, IRouter router) Staking(token, router) {} 11 | 12 | function distribute(uint256 worker, uint256 amount) external { 13 | lastEpochRewarded = router.networkController().epochNumber(); 14 | _distribute(worker, amount); 15 | } 16 | } 17 | 18 | contract StakersRewardDistributionTest is BaseTest { 19 | uint256[] workers = [1234]; 20 | StakingHelper staking; 21 | IERC20 token; 22 | NetworkController network; 23 | 24 | function setUp() public { 25 | (SQD _token, Router router) = deployAll(); 26 | token = _token; 27 | network = NetworkController(address(router.networkController())); 28 | network.setEpochLength(2); 29 | staking = new StakingHelper(token, router); 30 | router.setStaking(staking); 31 | token.transfer(address(1), token.totalSupply() / 2); 32 | token.approve(address(staking), type(uint256).max); 33 | hoax(address(1)); 34 | token.approve(address(staking), type(uint256).max); 35 | staking.grantRole(staking.REWARDS_DISTRIBUTOR_ROLE(), address(this)); 36 | vm.mockCall( 37 | address(router.workerRegistration()), 38 | abi.encodeWithSelector(WorkerRegistration.isWorkerActive.selector), 39 | abi.encode(true) 40 | ); 41 | } 42 | 43 | function assertPairClaimable(uint256 rewardA, uint256 rewardB) internal { 44 | assertEq(staking.claimable(address(this)), rewardA); 45 | assertEq(staking.claimable(address(1)), rewardB); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/contracts/test/WorkerRegistration/WorkerRegistration.constructor.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./WorkerRegistration.sol"; 5 | 6 | contract WorkerRegistrationConstructorTest is WorkerRegistrationTest { 7 | function testConstructor() public { 8 | assertEq(address(workerRegistration.SQD()), address(token)); 9 | assertEq(workerRegistration.lockPeriod(), EPOCH_LENGTH); 10 | } 11 | 12 | function test_CorrectlyCountsEpochStart() public { 13 | assertEq(workerRegistration.nextEpoch(), 7); 14 | vm.roll(block.number + 1); 15 | assertEq(workerRegistration.nextEpoch(), 7); 16 | vm.roll(block.number + 1); 17 | assertEq(workerRegistration.nextEpoch(), 9); 18 | vm.roll(block.number + 1); 19 | assertEq(workerRegistration.nextEpoch(), 9); 20 | vm.roll(block.number + 1); 21 | assertEq(workerRegistration.nextEpoch(), 11); 22 | vm.roll(block.number + 1); 23 | assertEq(workerRegistration.nextEpoch(), 11); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/contracts/test/WorkerRegistration/WorkerRegistration.deregister.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./WorkerRegistration.sol"; 5 | 6 | contract WorkerRegistrationDeregisterTest is WorkerRegistrationTest { 7 | function testRevertsIfWorkerIsNotRegistered() public { 8 | vm.expectRevert("Worker not registered"); 9 | workerRegistration.deregister(workerId); 10 | } 11 | 12 | function testRevertsIfWorkerIsNotYetActive() public { 13 | workerRegistration.register(workerId); 14 | vm.expectRevert("Worker not active"); 15 | workerRegistration.deregister(workerId); 16 | } 17 | 18 | function testRevertsIfNotCalledByCreator() public { 19 | workerRegistration.register(workerId); 20 | jumpEpoch(); 21 | startHoax(address(123)); 22 | vm.expectRevert("Not worker creator"); 23 | workerRegistration.deregister(workerId); 24 | } 25 | 26 | function testRevertsIfWorkerDeregisteredTwice() public { 27 | workerRegistration.register(workerId); 28 | jumpEpoch(); 29 | workerRegistration.deregister(workerId); 30 | 31 | jumpEpoch(); 32 | vm.expectRevert("Worker not active"); 33 | workerRegistration.deregister(workerId); 34 | } 35 | 36 | function testSetsDeregisteredBlock() public { 37 | workerRegistration.register(workerId); 38 | jumpEpoch(); 39 | workerRegistration.deregister(workerId); 40 | (,,,, uint128 deregisteredAt,) = workerRegistration.workers(1); 41 | assertEq(deregisteredAt, nextEpoch()); 42 | } 43 | 44 | function testEmitsDeregisteredEvent() public { 45 | workerRegistration.register(workerId); 46 | jumpEpoch(); 47 | vm.expectEmit(address(workerRegistration)); 48 | emit WorkerDeregistered(1, creator, nextEpoch()); 49 | workerRegistration.deregister(workerId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/contracts/test/WorkerRegistration/WorkerRegistration.excessiveBond.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./WorkerRegistration.sol"; 5 | 6 | contract WorkerRegistrationExcessiveBondTest is WorkerRegistrationTest { 7 | function test_ReturnExcessiveBondReturnsExcessiveBondForWorker() public { 8 | workerRegistration.register(workerId); 9 | jumpEpoch(); 10 | networkController.setBondAmount(6 ether); 11 | uint256 balanceBefore = token.balanceOf(address(creator)); 12 | workerRegistration.returnExcessiveBond(workerId); 13 | assertEq(token.balanceOf(address(creator)), balanceBefore + 4 ether); 14 | } 15 | 16 | function test_CannotReturnSameBondTwice() public { 17 | workerRegistration.register(workerId); 18 | jumpEpoch(); 19 | networkController.setBondAmount(60); 20 | workerRegistration.returnExcessiveBond(workerId); 21 | uint256 balanceBefore = token.balanceOf(address(creator)); 22 | workerRegistration.returnExcessiveBond(workerId); 23 | assertEq(token.balanceOf(address(creator)), balanceBefore); 24 | } 25 | 26 | function test_RevertsIf_NotCalledByCreator() public { 27 | workerRegistration.register(workerId); 28 | jumpEpoch(); 29 | networkController.setBondAmount(60); 30 | vm.expectRevert("Not worker creator"); 31 | vm.startPrank(address(1)); 32 | workerRegistration.returnExcessiveBond(workerId); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/contracts/test/WorkerRegistration/WorkerRegistration.register.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./WorkerRegistration.sol"; 5 | 6 | contract WorkerRegistrationRegisterTest is WorkerRegistrationTest { 7 | function test_RegisterWorkerTransfersToken() public { 8 | uint256 registrationBalanceBefore = token.balanceOf(address(workerRegistration)); 9 | workerRegistration.register(workerId); 10 | uint256 registrationBalanceAfter = token.balanceOf(address(workerRegistration)); 11 | assertEq(registrationBalanceAfter, registrationBalanceBefore + workerRegistration.bondAmount()); 12 | } 13 | 14 | function test_RegisterWorkerEmitsEvent() public { 15 | vm.expectEmit(address(workerRegistration)); 16 | emit WorkerRegistered(1, workerId, creator, nextEpoch(), "metadata"); 17 | workerRegistration.register(workerId, "metadata"); 18 | } 19 | 20 | function test_MakesWorkerActive() public { 21 | assertEq(workerRegistration.isWorkerActive(1), false); 22 | workerRegistration.register(workerId); 23 | assertEq(workerRegistration.isWorkerActive(1), false); 24 | jumpEpoch(); 25 | assertEq(workerRegistration.isWorkerActive(1), true); 26 | } 27 | 28 | function test_RevertsIfSameWorkedRegisteredTwice() public { 29 | workerRegistration.register(workerId); 30 | vm.expectRevert("Worker already exists"); 31 | workerRegistration.register(workerId); 32 | } 33 | 34 | function test_RevertsIfPeerIdIsOver64Bytes() public { 35 | bytes memory idWith64Bytes = abi.encodePacked(uint256(1), uint256(2)); 36 | workerRegistration.register(idWith64Bytes); 37 | bytes memory idWith65Bytes = abi.encodePacked(uint256(1), uint256(2), true); 38 | vm.expectRevert("Peer ID too large"); 39 | workerRegistration.register(idWith65Bytes); 40 | } 41 | 42 | function test_IncrementsIdForNextWorker() public { 43 | token.approve(address(workerRegistration), workerRegistration.bondAmount() * 2); 44 | 45 | workerRegistration.register(workerId); 46 | workerRegistration.register(workerId2); 47 | assertEq(workerRegistration.workerIds(workerId2), 2); 48 | } 49 | 50 | function test_CorrectlyCreatesWorkerStruct() public { 51 | workerRegistration.register(workerId, "metadata"); 52 | 53 | WorkerRegistration.Worker memory workerStruct = workerRegistration.getWorker(1); 54 | assertEq(workerStruct.creator, creator); 55 | assertEq(workerStruct.peerId, workerId); 56 | assertEq(workerStruct.bond, workerRegistration.bondAmount()); 57 | assertEq(workerStruct.registeredAt, nextEpoch()); 58 | assertEq(workerStruct.deregisteredAt, 0); 59 | assertEq(workerStruct.metadata, "metadata"); 60 | assertEq(workerRegistration.getMetadata(workerId), "metadata"); 61 | } 62 | 63 | function test_RegisteredWorkerAppearsInActiveWorkersAfterNextEpochStart() public { 64 | workerRegistration.register(workerId); 65 | assertEq(workerRegistration.getActiveWorkerCount(), 0); 66 | jumpEpoch(); 67 | assertEq(workerRegistration.getActiveWorkerCount(), 1); 68 | assertEq(workerRegistration.getActiveWorkerIds()[0], 1); 69 | assertEq(workerRegistration.getActiveWorkers()[0].peerId, workerId); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/contracts/test/WorkerRegistration/WorkerRegistration.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "../../src/WorkerRegistration.sol"; 5 | import "../../src/SQD.sol"; 6 | import "../../src/NetworkController.sol"; 7 | import "../../src/Staking.sol"; 8 | import "../BaseTest.sol"; 9 | 10 | contract WorkerRegistrationTest is BaseTest { 11 | uint128 constant EPOCH_LENGTH = 2; 12 | WorkerRegistration public workerRegistration; 13 | NetworkController public networkController; 14 | Staking public staking; 15 | IERC20 public token; 16 | 17 | address creator = address(this); 18 | bytes public workerId = "test-peer-id-1"; 19 | bytes public workerId2 = "test-peer-id-2"; 20 | 21 | event WorkerRegistered( 22 | uint256 indexed workerId, bytes peerId, address indexed registrar, uint256 registeredAt, string metadata 23 | ); 24 | event WorkerDeregistered(uint256 indexed workerId, address indexed account, uint256 deregistedAt); 25 | event WorkerWithdrawn(uint256 indexed workerId, address indexed account); 26 | event Delegated(uint256 indexed workerId, address indexed staker, uint256 amount); 27 | event Unstaked(uint256 indexed workerId, address indexed staker, uint256 amount); 28 | 29 | function nextEpoch() internal view returns (uint128) { 30 | return ((uint128(block.number) - 5) / 2 + 1) * 2 + 5; 31 | } 32 | 33 | function jumpEpoch() internal { 34 | vm.roll(block.number + 2); 35 | } 36 | 37 | function setUp() public { 38 | (SQD _token, Router router) = deployAll(); 39 | token = _token; 40 | workerRegistration = WorkerRegistration(address(router.workerRegistration())); 41 | networkController = NetworkController(address(router.networkController())); 42 | networkController.setEpochLength(EPOCH_LENGTH); 43 | networkController.setLockPeriod(EPOCH_LENGTH); 44 | vm.roll(workerRegistration.nextEpoch()); 45 | staking = Staking(address(router.staking())); 46 | token.approve(address(workerRegistration), workerRegistration.bondAmount()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/contracts/test/WorkerRegistration/WorkerRegistration.updateMetadata.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.20; 3 | 4 | import "./WorkerRegistration.sol"; 5 | 6 | contract WorkerRegistrationUpdateMetadataTest is WorkerRegistrationTest { 7 | function test_RevertsIf_NotWorkerCreator() public { 8 | workerRegistration.register(workerId); 9 | hoax(address(1)); 10 | vm.expectRevert("Not worker creator"); 11 | workerRegistration.updateMetadata(workerId, "new metadata"); 12 | } 13 | 14 | function test_UpdatesMetadata() public { 15 | workerRegistration.register(workerId); 16 | workerRegistration.updateMetadata(workerId, "new metadata"); 17 | assertEq(workerRegistration.getMetadata(workerId), "new metadata"); 18 | } 19 | 20 | event MetadataUpdated(uint256 indexed workerId, string metadata); 21 | 22 | function test_EmitsEvent() public { 23 | workerRegistration.register(workerId); 24 | vm.expectEmit(address(workerRegistration)); 25 | emit MetadataUpdated(1, "new metadata"); 26 | workerRegistration.updateMetadata(workerId, "new metadata"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/contracts/test/strategies/EqualStrategy.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "../BaseTest.sol"; 4 | import "../../src/GatewayRegistry.sol"; 5 | import "../../src/gateway-strategies/EqualStrategy.sol"; 6 | 7 | contract EqualStrategyTest is BaseTest { 8 | bytes peerId = "gateway-peerId"; 9 | 10 | function test_EquallyDividesComputationUnits() public { 11 | (IERC20 token, Router router) = deployAll(); 12 | 13 | vm.mockCall( 14 | address(gatewayRegistry.router().workerRegistration()), 15 | abi.encodeWithSelector(WorkerRegistration.getActiveWorkerCount.selector), 16 | abi.encode(15) 17 | ); 18 | vm.mockCall( 19 | address(gatewayRegistry), 20 | abi.encodeWithSelector(IGatewayRegistry.computationUnitsAvailable.selector, peerId), 21 | abi.encode(300_000) 22 | ); 23 | EqualStrategy strategy = new EqualStrategy(gatewayRegistry.router(), gatewayRegistry); 24 | assertEq(strategy.computationUnitsPerEpoch(peerId, 0), 20_000); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "ts-node": { 15 | "esm": true, 16 | "experimentalSpecifierResolution": "node", 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/reward-stats/.env.example: -------------------------------------------------------------------------------- 1 | VITE_ALCHEMY_API_KEY= -------------------------------------------------------------------------------- /packages/reward-stats/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | 11 | /coverage 12 | 13 | # vite 14 | 15 | dist 16 | dist-ssr 17 | 18 | # production 19 | 20 | /build 21 | 22 | # misc 23 | 24 | .DS_Store 25 | \*.pem 26 | *.local 27 | 28 | # debug 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | .pnpm-debug.log* 34 | 35 | # local env files 36 | 37 | .env 38 | .env\*.local 39 | 40 | # vercel 41 | 42 | .vercel 43 | 44 | # typescript 45 | 46 | \*.tsbuildinfo 47 | next-env.d.ts 48 | -------------------------------------------------------------------------------- /packages/reward-stats/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies = false -------------------------------------------------------------------------------- /packages/reward-stats/README.md: -------------------------------------------------------------------------------- 1 | This is a [wagmi](https://wagmi.sh) + [Vite](https://vitejs.dev/) project bootstrapped with [`create-wagmi`](https://github.com/wagmi-dev/wagmi/tree/main/packages/create-wagmi) 2 | 3 | # Getting Started 4 | 5 | Run `pnpm run dev` in your terminal, and then open [localhost:5173](http://localhost:5173) in your browser. 6 | 7 | Once the webpage has loaded, changes made to files inside the `src/` directory (e.g. `src/App.tsx`) will automatically update the webpage. 8 | 9 | # Learn more 10 | 11 | To learn more about [Vite](https://vitejs.dev/) or [wagmi](https://wagmi.sh), check out the following resources: 12 | 13 | - [wagmi Documentation](https://wagmi.sh) – learn about wagmi Hooks and API. 14 | - [wagmi Examples](https://wagmi.sh/examples/connect-wallet) – a suite of simple examples using wagmi. 15 | - [Vite Documentation](https://vitejs.dev/) – learn about Vite features and API. 16 | -------------------------------------------------------------------------------- /packages/reward-stats/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subsquid/subsquid-network-contracts/23cfcce34ce344ccf68ef550bbc09a5b0a0ec6d0/packages/reward-stats/assets/logo.png -------------------------------------------------------------------------------- /packages/reward-stats/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Subsquid worker rewards 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/reward-stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@subsquid-network/reward-stats", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node_modules/.bin/vite", 7 | "build": "node_modules/.bin/tsc && vite build", 8 | "preview": "node_modules/.bin/vite preview", 9 | "predeploy": "pnpm run build", 10 | "deploy": "gh-pages -d dist", 11 | "lint:fix": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" 12 | }, 13 | "dependencies": { 14 | "@clickhouse/client-web": "^0.2.3", 15 | "@subsquid-network/rewards-calculator": "workspace:*", 16 | "@wagmi/core": "^1.4.13", 17 | "abitype": "^0.10.1", 18 | "buffer": "^6.0.3", 19 | "process": "^0.11.10", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "recharts": "^2.8.0", 23 | "util": "^0.12.4", 24 | "viem": "~0.3.36", 25 | "wagmi": "^1.4.13" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.0.9", 29 | "@types/react-dom": "^18.0.3", 30 | "@vitejs/plugin-react": "^4.0.0", 31 | "autoprefixer": "^10.4.16", 32 | "gh-pages": "^6.0.0", 33 | "postcss": "^8.4.31", 34 | "prettier": "^3.0.3", 35 | "prettier-plugin-tailwindcss": "^0.5.6", 36 | "tailwindcss": "^3.3.3", 37 | "typescript": "^5.0.4", 38 | "vite": "^4.3.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/reward-stats/polyfills.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer' 2 | import process from 'process' 3 | 4 | window.global = window 5 | window.process = process 6 | window.Buffer = Buffer 7 | 8 | declare global { 9 | interface Window { 10 | global: Window; 11 | process: typeof process, 12 | Buffer: typeof Buffer 13 | } 14 | } 15 | 16 | // @ts-ignore 17 | Object.defineProperty(BigInt.prototype, "toJSON", { 18 | get() { 19 | "use strict"; 20 | return () => String(this); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /packages/reward-stats/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/reward-stats/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier-plugin-tailwindcss"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/reward-stats/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRewards } from "./hooks/useRewards"; 2 | import { useState } from "react"; 3 | import { RewardLinks } from "./components/RewardLinks"; 4 | import { Stats } from "./components/Stats"; 5 | 6 | export function App() { 7 | const rewards = useRewards(); 8 | const [selectedReward, setSelectedReward] = useState(0); 9 | 10 | return ( 11 |
12 |
13 |

14 | fromBlock - toBlock 15 |

16 |
17 | 22 |
23 |
24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/reward-stats/src/components/RewardLinks.tsx: -------------------------------------------------------------------------------- 1 | import { Rewards } from "../hooks/useRewards"; 2 | import { formatSqd, bigIntToDecimal } from "@subsquid-network/rewards-calculator/src/utils"; 3 | 4 | interface RewardLinkProps { 5 | rewards: Rewards[]; 6 | onClick(idx: number): void; 7 | selected: number; 8 | } 9 | 10 | export const RewardLinks = ({ 11 | rewards, 12 | onClick, 13 | selected, 14 | }: RewardLinkProps) => ( 15 | <> 16 | {rewards.map((reward, idx) => ( 17 |
onClick(idx)} 23 | > 24 | {Number(reward.fromBlock)} - {Number(reward.toBlock)} (Rewarded:{" "} 25 | {formatSqd(bigIntToDecimal(reward.totalReward))}) 26 |
27 | ))} 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /packages/reward-stats/src/components/RewardsChart.tsx: -------------------------------------------------------------------------------- 1 | // recharts bar chart component 2 | import React from "react"; 3 | import { 4 | Bar, 5 | BarChart, 6 | CartesianGrid, 7 | Legend, 8 | ResponsiveContainer, 9 | Tooltip, 10 | XAxis, 11 | YAxis, 12 | } from "recharts"; 13 | import { Workers } from "../hooks/useWorkers"; 14 | import { Stakes } from "../hooks/useStakes"; 15 | import { formatToken } from "../utils/formatToken"; 16 | import { useBond } from "../hooks/useBond"; 17 | import { toNumber } from "../utils/toNumber"; 18 | 19 | interface Reward { 20 | workerId: number; 21 | stakerReward: number; 22 | workerReward: number; 23 | } 24 | 25 | interface RewardsChartProps { 26 | rewards: Reward[]; 27 | workers?: Workers; 28 | stakes: Stakes; 29 | timeDiff: number; 30 | } 31 | 32 | export const RewardsChart = ({ 33 | rewards, 34 | workers, 35 | stakes, 36 | timeDiff, 37 | }: RewardsChartProps) => { 38 | const CustomTooltip = ({ 39 | active, 40 | payload, 41 | label, 42 | }: { 43 | payload?: [{ payload: Reward }]; 44 | active?: boolean; 45 | label?: number; 46 | }) => { 47 | const year = 1000 * 60 * 60 * 24 * 365; 48 | if (active && payload && label) { 49 | return ( 50 |
51 |

52 | Worker "{workers?.[label]?.metadata.name}" (#{label}) 53 |

54 |

{workers?.[label]?.peerId}

55 |

{workers?.[label]?.metadata.description}

56 |

{workers?.[label]?.metadata.email}

57 |

58 | Worker reward: {payload[0].payload.workerReward} 59 |  (bond: {formatToken(bond)}) 60 |

61 |

62 | Staker reward: {payload[0].payload.stakerReward} 63 |  (staked: {formatToken(stakes[label])}) 64 |

65 |

66 | Worker APY:{" "} 67 | {( 68 | (100 * payload[0].payload.workerReward * year) / 69 | toNumber(bond) / 70 | timeDiff 71 | ).toFixed(2)} 72 | % 73 |

74 | {!!stakes[label] && ( 75 |

76 | Staker APY:{" "} 77 | {( 78 | (100 * payload[0].payload.stakerReward * year) / 79 | toNumber(stakes[label]) / 80 | timeDiff 81 | ).toFixed(2)} 82 | % 83 |

84 | )} 85 |
86 | ); 87 | } 88 | }; 89 | 90 | const bond = useBond(); 91 | 92 | return ( 93 | 94 | 95 | 96 | 97 | 98 | } /> 99 | 100 | 101 | 102 | 103 | 104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /packages/reward-stats/src/config/contracts.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "wagmi"; 2 | import Deployments from "../../../contracts/deployments/421614.json"; 3 | import rewardsDistributionAbi from "../../../contracts/artifacts/DistributedRewardDistribution.sol/DistributedRewardsDistribution"; 4 | import workerRegistrationAbi from "../../../contracts/artifacts/WorkerRegistration.sol/WorkerRegistration"; 5 | import stakingAbi from "../../../contracts/artifacts/Staking.sol/Staking"; 6 | 7 | export const distributorContractConfig = { 8 | address: Deployments.DistributedRewardsDistribution as Address, 9 | abi: rewardsDistributionAbi.abi, 10 | }; 11 | 12 | export const workerRegistrationContractConfig = { 13 | address: Deployments.WorkerRegistration as Address, 14 | abi: workerRegistrationAbi.abi, 15 | }; 16 | 17 | export const stakingContractConfig = { 18 | address: Deployments.Staking as Address, 19 | abi: stakingAbi.abi, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/reward-stats/src/hooks/useBlockTimestamp.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { usePublicClient } from "wagmi"; 3 | import { sepolia } from "wagmi/chains"; 4 | 5 | export function useBlocksTimestamp( 6 | fromBlock: bigint | undefined, 7 | toBlock: bigint | undefined, 8 | ) { 9 | const publicClient = usePublicClient({ 10 | chainId: sepolia.id, 11 | }); 12 | const [fromTimestamp, setFromTimestamp] = useState(0); 13 | const [toTimestamp, setToTimestamp] = useState(0); 14 | 15 | useEffect(() => { 16 | publicClient 17 | .getBlock({ 18 | blockNumber: fromBlock, 19 | }) 20 | .then((block) => setFromTimestamp(Number(block.timestamp) * 1000)); 21 | publicClient 22 | .getBlock({ 23 | blockNumber: toBlock, 24 | }) 25 | .then((block) => setToTimestamp(Number(block.timestamp) * 1000)); 26 | }, [fromBlock, toBlock]); 27 | 28 | return { fromTimestamp, toTimestamp, timeDiff: toTimestamp - fromTimestamp }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/reward-stats/src/hooks/useBond.ts: -------------------------------------------------------------------------------- 1 | import { useContractRead } from "wagmi"; 2 | import { workerRegistrationContractConfig } from "../config/contracts"; 3 | 4 | export function useBond() { 5 | return ( 6 | useContractRead({ 7 | ...workerRegistrationContractConfig, 8 | functionName: "bondAmount", 9 | })?.data ?? 0n 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/reward-stats/src/hooks/useRewards.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Abi } from "viem"; 3 | import { distributorContractConfig } from "../config/contracts"; 4 | import { Address, UseContractEventConfig, usePublicClient } from "wagmi"; 5 | import { AbiEvent } from "abitype/src/abi"; 6 | import { bigSum } from "@subsquid-network/rewards-calculator/src/utils"; 7 | 8 | function getEventByName( 9 | abi: TAbi, 10 | eventName: UseContractEventConfig["eventName"], 11 | ): AbiEvent { 12 | return abi.find( 13 | (event) => event.type === "event" && event.name === eventName, 14 | ) as AbiEvent; 15 | } 16 | 17 | export interface Rewards { 18 | fromBlock: bigint; 19 | toBlock: bigint; 20 | who: Address; 21 | recipients: bigint[]; 22 | workerRewards: bigint[]; 23 | stakerRewards: bigint[]; 24 | totalReward: bigint; 25 | } 26 | 27 | export const useRewards = () => { 28 | const [rewards, setRewards] = useState([]); 29 | const publicClient = usePublicClient(); 30 | 31 | useEffect(() => { 32 | (async () => { 33 | const distributions = await publicClient.getLogs({ 34 | ...distributorContractConfig, 35 | event: getEventByName(distributorContractConfig.abi, "Distributed"), 36 | fromBlock: 0n, 37 | }); 38 | const _rewards = distributions 39 | .map(({ args }: any) => ({ 40 | ...args, 41 | totalReward: bigSum(args.workerRewards) + bigSum(args.stakerRewards), 42 | })) 43 | .reverse() as any; 44 | 45 | setRewards(_rewards); 46 | })(); 47 | }, []); 48 | 49 | return rewards; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/reward-stats/src/hooks/useStakes.ts: -------------------------------------------------------------------------------- 1 | import { Rewards } from "./useRewards"; 2 | import { allWorkerIds } from "../utils/allWorkerIds"; 3 | import { useContractReads } from "wagmi"; 4 | import { stakingContractConfig } from "../config/contracts"; 5 | 6 | export type Stakes = { 7 | [key: number]: bigint; 8 | }; 9 | export const useStakes = (rewards: Rewards[]): Stakes => { 10 | const allWorkers = allWorkerIds(rewards); 11 | 12 | const stakes = useContractReads({ 13 | contracts: allWorkerIds(rewards).map((id) => ({ 14 | ...stakingContractConfig, 15 | functionName: "activeStake", 16 | args: [[id]], 17 | })), 18 | }); 19 | 20 | return Object.fromEntries( 21 | allWorkers.map((id, index) => [ 22 | id, 23 | BigInt((stakes.data?.[index]?.result as any) ?? 0), 24 | ]), 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/reward-stats/src/hooks/useWorkers.ts: -------------------------------------------------------------------------------- 1 | import { Rewards } from "./useRewards"; 2 | import { Address, useContractReads } from "wagmi"; 3 | import { workerRegistrationContractConfig } from "../config/contracts"; 4 | import { toBase58 } from "@subsquid-network/rewards-calculator/src/utils"; 5 | import { allWorkerIds } from "../utils/allWorkerIds"; 6 | 7 | export interface Worker { 8 | peerId: string; 9 | metadata: any; 10 | creator: Address; 11 | } 12 | 13 | export interface Workers { 14 | [id: number]: Worker; 15 | } 16 | 17 | export const useWorkers = (rewards: Rewards[]): Workers | undefined => { 18 | const ids = allWorkerIds(rewards); 19 | const workerIds = useContractReads({ 20 | contracts: ids.map((id) => ({ 21 | ...workerRegistrationContractConfig, 22 | functionName: "getWorkerByIndex", 23 | args: [id], 24 | })), 25 | }); 26 | 27 | const workersData = workerIds.data?.map(({ result }) => 28 | result 29 | ? ({ 30 | ...result, 31 | peerId: toBase58((result as any).peerId), 32 | metadata: JSON.parse((result as any).metadata), 33 | } as Worker) 34 | : undefined, 35 | ); 36 | 37 | return Object.fromEntries( 38 | workersData?.map((worker, idx) => [ids[idx] + 1, worker]) ?? [], 39 | ) as any; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/reward-stats/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/reward-stats/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | 4 | import { App } from "./App"; 5 | import { config } from "./wagmi"; 6 | import { WagmiConfig } from "wagmi"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /packages/reward-stats/src/utils/allWorkerIds.ts: -------------------------------------------------------------------------------- 1 | import { Rewards } from "../hooks/useRewards"; 2 | 3 | export const allWorkerIds = (rewards: Rewards[]) => { 4 | const maxWorkerId = rewards.reduce( 5 | (max, { recipients }) => 6 | recipients.reduce((_max, id) => (id > _max ? id : _max), max), 7 | 0n, 8 | ); 9 | 10 | return [...Array(Number(maxWorkerId) + 1).keys()].slice(1); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/reward-stats/src/utils/formatToken.ts: -------------------------------------------------------------------------------- 1 | import { toNumber } from "./toNumber"; 2 | 3 | export const formatToken = (amount: bigint) => `${toNumber(amount)} SQD`; 4 | -------------------------------------------------------------------------------- /packages/reward-stats/src/utils/stringify.ts: -------------------------------------------------------------------------------- 1 | export const stringify: typeof JSON.stringify = (value, replacer, space) => 2 | JSON.stringify( 3 | value, 4 | (key, value_) => { 5 | const value = typeof value_ === "bigint" ? value_.toString() : value_; 6 | return typeof replacer === "function" ? replacer(key, value) : value; 7 | }, 8 | space, 9 | ); 10 | -------------------------------------------------------------------------------- /packages/reward-stats/src/utils/toNumber.tsx: -------------------------------------------------------------------------------- 1 | import { formatEther } from "viem"; 2 | 3 | export const toNumber = (eth: bigint) => Number(formatEther(eth)); 4 | -------------------------------------------------------------------------------- /packages/reward-stats/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/reward-stats/src/wagmi.ts: -------------------------------------------------------------------------------- 1 | import { sepolia } from "wagmi/chains"; 2 | import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet"; 3 | import { InjectedConnector } from "wagmi/connectors/injected"; 4 | import { MetaMaskConnector } from "wagmi/connectors/metaMask"; 5 | 6 | import { publicProvider } from "wagmi/providers/public"; 7 | import { configureChains, createConfig } from "wagmi"; 8 | import { Address } from "viem"; 9 | 10 | export const arbitrumSepolia = { 11 | id: 421614, 12 | name: "Arbitrum Sepolia", 13 | network: "arbitrum-sepolia", 14 | nativeCurrency: { 15 | name: "Arbitrum Sepolia Ether", 16 | symbol: "ETH", 17 | decimals: 18, 18 | }, 19 | rpcUrls: { 20 | alchemy: { 21 | http: ["https://arb-sepolia.g.alchemy.com/v2"], 22 | webSocket: ["wss://arb-sepolia.g.alchemy.com/v2"], 23 | }, 24 | infura: { 25 | http: ["https://arbitrum-sepolia.infura.io/v3"], 26 | webSocket: ["wss://arbitrum-sepolia.infura.io/ws/v3"], 27 | }, 28 | default: { 29 | http: ["https://sepolia-rollup.arbitrum.io/rpc"], 30 | }, 31 | public: { 32 | http: ["https://sepolia-rollup.arbitrum.io/rpc"], 33 | }, 34 | }, 35 | blockExplorers: { 36 | etherscan: { 37 | name: "Arbiscan", 38 | url: "https://sepolia.arbiscan.io/", 39 | }, 40 | default: { 41 | name: "Arbiscan", 42 | url: "https://sepolia.arbiscan.io/", 43 | }, 44 | }, 45 | contracts: { 46 | multicall3: { 47 | address: "0xcA11bde05977b3631167028862bE2a173976CA11" as Address, 48 | blockCreated: 81930, 49 | }, 50 | }, 51 | testnet: true, 52 | }; 53 | 54 | const { chains, publicClient, webSocketPublicClient } = configureChains( 55 | [arbitrumSepolia, sepolia], 56 | [publicProvider()], 57 | ); 58 | 59 | export const config = createConfig({ 60 | autoConnect: true, 61 | connectors: [ 62 | new MetaMaskConnector({ chains }), 63 | new CoinbaseWalletConnector({ 64 | chains, 65 | options: { 66 | appName: "wagmi", 67 | }, 68 | }), 69 | new InjectedConnector({ 70 | chains, 71 | options: { 72 | name: "Injected", 73 | shimDisconnect: true, 74 | }, 75 | }), 76 | ], 77 | publicClient, 78 | webSocketPublicClient, 79 | }); 80 | -------------------------------------------------------------------------------- /packages/reward-stats/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /packages/reward-stats/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "react-jsx", 9 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "noEmit": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "target": "ESNext", 17 | "useDefineForClassFields": true 18 | }, 19 | "include": [ 20 | "./src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/reward-stats/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | define: { 7 | global: "globalThis", 8 | }, 9 | resolve: { 10 | alias: { 11 | process: "process/browser", 12 | util: "util", 13 | }, 14 | }, 15 | plugins: [react()], 16 | }); 17 | -------------------------------------------------------------------------------- /packages/rewards-calculator/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | README.md 4 | Dockerfile 5 | -------------------------------------------------------------------------------- /packages/rewards-calculator/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | cache 3 | node_modules 4 | .idea 5 | pings.csv 6 | queries.csv 7 | stakes.csv 8 | *.pem 9 | -------------------------------------------------------------------------------- /packages/rewards-calculator/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": ["ts"], 3 | "node-option": [ 4 | "loader=ts-node/esm" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/rewards-calculator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/foundry-rs/foundry:stable 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add nodejs-current yarn npm bash 6 | COPY packages/contracts/foundry.toml ./contracts/foundry.toml 7 | COPY packages/contracts/deployments ./contracts/deployments 8 | COPY packages/contracts/src ./contracts/src 9 | COPY packages/contracts/package.json ./contracts/package.json 10 | 11 | WORKDIR /usr/src/app/contracts 12 | RUN forge install foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts OpenZeppelin/openzeppelin-contracts-upgradeable PaulRBerg/prb-math@release-v4 --no-git 13 | RUN yarn build 14 | WORKDIR /usr/src/app 15 | COPY packages/rewards-calculator ./rewards-calculator 16 | WORKDIR /usr/src/app/rewards-calculator 17 | RUN yarn 18 | 19 | CMD ["yarn start 1"] 20 | 21 | -------------------------------------------------------------------------------- /packages/rewards-calculator/Dockerfile-endpoints: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/foundry-rs/foundry:latest 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add nodejs-current yarn npm bash 6 | COPY packages/contracts/foundry.toml ./contracts/foundry.toml 7 | COPY packages/contracts/deployments ./contracts/deployments 8 | COPY packages/contracts/src ./contracts/src 9 | COPY packages/contracts/package.json ./contracts/package.json 10 | 11 | WORKDIR /usr/src/app/contracts 12 | RUN forge install foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts OpenZeppelin/openzeppelin-contracts-upgradeable PaulRBerg/prb-math@release-v4 --no-git 13 | RUN yarn build 14 | WORKDIR /usr/src/app 15 | COPY packages/rewards-calculator ./rewards-calculator 16 | WORKDIR /usr/src/app/rewards-calculator 17 | RUN yarn 18 | 19 | CMD ["yarn endpoints"] 20 | -------------------------------------------------------------------------------- /packages/rewards-calculator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@subsquid-network/rewards-calculator", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "engines": { 6 | "node": ">=16" 7 | }, 8 | "dependencies": { 9 | "@clickhouse/client-web": "^0.2.6", 10 | "@libp2p/crypto": "^3.0.0", 11 | "@libp2p/peer-id": "^4.0.0", 12 | "bs58": "^5.0.0", 13 | "clickhouse": "^2.6.0", 14 | "dayjs": "^1.11.10", 15 | "decimal.js": "^10.4.3", 16 | "express": "^4.19.2", 17 | "libp2p": "^1.0.0", 18 | "protobufjs": "^7.2.5", 19 | "viem": "^1.19.10" 20 | }, 21 | "scripts": { 22 | "reward-simulation": "ts-node src/generate-logs.ts && ts-node src/index.ts", 23 | "start": "TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"--loader ts-node/esm\" ts-node src/index.ts", 24 | "endpoints": "TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"--loader ts-node/esm\" ts-node src/endpoints.ts", 25 | "stats": "ts-node src/epochStats.ts" 26 | }, 27 | "devDependencies": { 28 | "@types/chai": "^4.3.4", 29 | "@types/express": "^4.17.21", 30 | "@types/mocha": "^10.0.6", 31 | "@types/node": "^20.10.1", 32 | "chai": "^4.3.10", 33 | "mocha": "^10.2.0", 34 | "ts-node": "^10.9.1", 35 | "typescript": "^5.3.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/epochStats.ts: -------------------------------------------------------------------------------- 1 | import { epochStats } from "./reward"; 2 | process.env.VERBOSE = "true"; 3 | const [, , fromBlock, toBlock] = process.argv.map(Number); 4 | // const workers = await epochStats(fromBlock, toBlock); 5 | // await workers.printLogs(); 6 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/fordefi/getAddress.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "viem"; 2 | 3 | export async function getVaultAddress(): Promise { 4 | const vaultId = process.env.FORDEFI_VAULT_ID; 5 | if (!vaultId) { 6 | throw new Error("FORDEFI_VAULT_ID is not set"); 7 | } 8 | const accessToken = process.env.FORDEFI_ACCESS_TOKEN; 9 | if (!accessToken) { 10 | throw new Error("FORDEFI_ACCESS_TOKEN is not set"); 11 | } 12 | const request = await fetch( 13 | `https://api.fordefi.com/api/v1/vaults/${vaultId}`, 14 | { 15 | headers: { 16 | Authorization: `Bearer ${accessToken}`, 17 | }, 18 | }, 19 | ); 20 | if (!request.ok) { 21 | throw new Error(await request.text()); 22 | } 23 | const data = await request.json(); 24 | return data.address; 25 | } 26 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/fordefi/request.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../config"; 2 | 3 | export function fordefiRequest(to: string, data: string, name: string) { 4 | const chain = 5 | config.network.networkName === "sepolia" 6 | ? "arbitrum_sepolia" 7 | : "arbitrum_mainnet"; 8 | 9 | return { 10 | signer_type: "api_signer", 11 | type: "evm_transaction", 12 | details: { 13 | type: "evm_raw_transaction", 14 | to, 15 | value: "0", 16 | gas: { 17 | type: "priority", 18 | priority_level: "medium", 19 | }, 20 | fail_on_prediction_failure: false, 21 | chain, 22 | data: { 23 | type: "hex", 24 | hex_data: data, 25 | }, 26 | }, 27 | note: name, 28 | vault_id: process.env.FORDEFI_VAULT_ID, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/fordefi/sendTransaction.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import fs from "fs"; 3 | import { config } from "../config"; 4 | import { Hex } from "viem"; 5 | 6 | const gatewayHost = "api.fordefi.com"; 7 | 8 | async function waitForFordefiTransaction(id: string) { 9 | const accessToken = config.fordefi.accessToken; 10 | const path = "/api/v1/transactions"; 11 | 12 | let timeout = 250; 13 | while (true) { 14 | const response = await fetch(`https://${gatewayHost}${path}/${id}`, { 15 | method: "GET", 16 | headers: { 17 | Accept: "application/json", 18 | Authorization: `Bearer ${accessToken}`, 19 | }, 20 | }); 21 | const json = await response.json(); 22 | if (json.hash && json.mined_result?.reversion?.state === "not_reverted") { 23 | return json.hash as Hex; 24 | } 25 | if (json.hash && json.mined_result?.reversion?.reason) { 26 | throw new Error( 27 | JSON.stringify({ 28 | id: json.hash, 29 | reason: json.mined_result?.reason, 30 | }), 31 | ); 32 | } 33 | if (timeout >= 30000) { 34 | throw new Error(`Transaction ${id} timeout`); 35 | } 36 | timeout *= 2; 37 | await new Promise((resolve) => setTimeout(resolve, timeout)); 38 | } 39 | } 40 | 41 | export async function sendFordefiTransaction(request: any) { 42 | const accessToken = config.fordefi.accessToken; 43 | 44 | const requestBody = JSON.stringify(request); 45 | const path = "/api/v1/transactions"; 46 | const privateKeyFile = config.fordefi.secretPath; 47 | const timestamp = new Date().getTime(); 48 | const payload = `${path}|${timestamp}|${requestBody}`; 49 | 50 | const secretPem = fs.readFileSync(privateKeyFile, "utf8"); 51 | const privateKey = crypto.createPrivateKey(secretPem); 52 | const sign = crypto.createSign("SHA256").update(payload, "utf8").end(); 53 | const signature = sign.sign(privateKey, "base64"); 54 | 55 | const response = await fetch(`https://${gatewayHost}${path}`, { 56 | method: "POST", 57 | headers: { 58 | "Content-Type": "application/json", 59 | Authorization: `Bearer ${accessToken}`, 60 | "X-Timestamp": timestamp.toString(), 61 | "X-Signature": signature, 62 | }, 63 | body: requestBody, 64 | }); 65 | 66 | if (!response.ok) { 67 | throw new Error(await response.text()); 68 | } 69 | 70 | const { id } = await response.json(); 71 | return waitForFordefiTransaction(id); 72 | } 73 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/index.ts: -------------------------------------------------------------------------------- 1 | import { startBot } from "./startBot"; 2 | 3 | const n: number = Number(process.argv[2]); 4 | 5 | (async () => { 6 | for (let i = 0; i < n; i++) { 7 | await startBot(i); 8 | } 9 | })(); 10 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/logger.ts: -------------------------------------------------------------------------------- 1 | const shouldLog = () => process.env.VERBOSE === "true"; 2 | 3 | function logWithWorkerAddress( 4 | workerAddress: string, 5 | fun: "log" | "error", 6 | ...args: any[] 7 | ) { 8 | if (workerAddress !== "") { 9 | console[fun](`[${workerAddress}]`, ...args); 10 | } else { 11 | console[fun](...args); 12 | } 13 | } 14 | 15 | export const logger = { 16 | log(...args: any[]) { 17 | shouldLog() && logWithWorkerAddress(this.workerAddress, "log", ...args); 18 | }, 19 | error(...args: any[]) { 20 | shouldLog() && logWithWorkerAddress(this.workerAddress, "error", ...args); 21 | }, 22 | table: (...args: any[]) => shouldLog() && console.table(...args), 23 | workerAddress: "", 24 | }; 25 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/protobuf/query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Query { // Optional fields enforce serializing default values 4 | optional string query_id = 1; 5 | optional string dataset = 2; 6 | optional string query = 3; 7 | optional bool profiling = 4; 8 | optional string client_state_json = 5; 9 | bytes signature = 6; 10 | } 11 | 12 | message QueryExecuted { 13 | string client_id = 1; 14 | string worker_id = 2; 15 | 16 | Query query = 3; 17 | bytes query_hash = 5; 18 | 19 | optional uint32 exec_time_ms = 6; // optional to enforce serializing 0 20 | oneof result { 21 | InputAndOutput ok = 7; 22 | string bad_request = 8; 23 | string server_error = 9; 24 | } 25 | optional uint64 seq_no = 10; // optional to enforce serializing 0 26 | optional uint64 timestamp_ms = 11; // optional to enforce serializing 0 27 | bytes signature = 12; 28 | } 29 | 30 | 31 | message InputAndOutput { 32 | optional uint32 num_read_chunks = 1; // optional to enforce serializing 0 33 | SizeAndHash output = 2; 34 | } 35 | 36 | message SizeAndHash { 37 | optional uint32 size = 1; // optional to enforce serializing 0 38 | bytes sha3_256 = 2; 39 | } 40 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/reward.ts: -------------------------------------------------------------------------------- 1 | import { ClickhouseClient } from "./clickhouseClient"; 2 | import { logger } from "./logger"; 3 | import { getBlockTimestamp } from "./chain"; 4 | import { Workers } from "./workers"; 5 | 6 | export type Rewards = { 7 | [key in string]: { 8 | workerReward: bigint; 9 | stakerReward: bigint; 10 | computationUnitsUsed: number; 11 | id: bigint; 12 | }; 13 | }; 14 | 15 | export async function epochStats( 16 | fromBlock: number, 17 | toBlock: number, 18 | shouldSkipSignatureValidation = false, 19 | ): Promise { 20 | const from = await getBlockTimestamp(fromBlock); 21 | const to = await getBlockTimestamp(toBlock); 22 | logger.log(from, "-", to); 23 | const clickhouse = new ClickhouseClient(from, to); 24 | const workers = await clickhouse.getActiveWorkers(shouldSkipSignatureValidation); 25 | if (workers.count() === 0) { 26 | return workers; 27 | } 28 | await workers.getNextDistributionStartBlockNumber(); 29 | await workers.clearUnknownWorkers(); 30 | await workers.getStakes(); 31 | workers.getT(); 32 | await workers.fetchCurrentBond(); 33 | workers.getDTrraffic(); 34 | await workers.getLiveness(); 35 | await workers.getDTenure(fromBlock); 36 | await workers.calculateRewards(); 37 | await workers.logStats(); 38 | return workers; 39 | } 40 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/startBot.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | createWalletClient, 4 | fromHex, 5 | http, 6 | parseEther, 7 | PublicActions, 8 | publicActions, 9 | toHex, 10 | WalletClient, 11 | } from "viem"; 12 | import { arbitrumSepolia } from "viem/chains"; 13 | import { logger } from "./logger"; 14 | import { RewardBot } from "./rewardBot"; 15 | import { getVaultAddress } from "./fordefi/getAddress"; 16 | 17 | async function transferFundsIfNecessary( 18 | walletClient: WalletClient & PublicActions, 19 | from: Account, 20 | ) { 21 | const balance = await walletClient.getBalance({ 22 | address: walletClient.account!.address, 23 | }); 24 | logger.log("Balance", balance); 25 | if (balance === 0n) { 26 | logger.log("Funding account"); 27 | await createWalletClient({ 28 | chain: arbitrumSepolia, 29 | transport: http(), 30 | }).sendTransaction({ 31 | account: from, 32 | chain: arbitrumSepolia, 33 | to: walletClient.account!.address, 34 | value: parseEther("0.05"), 35 | }); 36 | } 37 | } 38 | 39 | export async function startBot(index: number) { 40 | const address = await getVaultAddress(); 41 | const bot = new RewardBot(address, index); 42 | bot.startBot(); 43 | } 44 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/testRPC.ts: -------------------------------------------------------------------------------- 1 | import { BlockTag, createPublicClient, http, parseAbiItem } from 'viem'; 2 | import { addresses, contracts, l1Client, publicClient } from './config'; 3 | import { logger } from './logger'; 4 | import { currentApy, getFirstBlockForL1Block, getLatestDistributionBlock } from './chain'; 5 | import { approveRanges } from './rewardBot'; 6 | 7 | export async function currentApyTest(blockNumber?: number) { 8 | let l2block: bigint | undefined; 9 | if (blockNumber) { 10 | l2block = await getFirstBlockForL1Block(blockNumber) 11 | } 12 | 13 | const l2blockNumber = l2block ? BigInt(l2block) : undefined 14 | try { 15 | //Directly read from the contract using the public client 16 | // const tvl = await client.readContract({ 17 | // address: addresses.rewardCalculation, 18 | // abi, 19 | // functionName: 'effectiveTVL', 20 | // l2blockNumber, 21 | // }) as bigint; 22 | console.log(`l1 block number: ${blockNumber}`) 23 | console.log(`l2 block number: ${l2blockNumber}`) 24 | const tvl = await contracts.rewardCalculation.read.effectiveTVL({blockNumber: l2blockNumber}); 25 | 26 | if (tvl === 0n) { 27 | return 2000n; 28 | } 29 | 30 | // const initialRewardPoolsSize = await client.readContract({ 31 | // address: addresses.rewardCalculation, 32 | // abi, 33 | // functionName: 'INITIAL_REWARD_POOL_SIZE', 34 | // blockNumber, 35 | // }) as bigint; 36 | 37 | const initialRewardPoolsSize = await contracts.rewardCalculation.read.INITIAL_REWARD_POOL_SIZE({blockNumber: l2blockNumber}); 38 | 39 | 40 | // const yearlyRewardCapCoefficient = await client.readContract({ 41 | // address: addresses.networkController, 42 | // abi, 43 | // functionName: 'yearlyRewardCapCoefficient', 44 | // blockNumber, 45 | // }) as bigint; 46 | 47 | const yearlyRewardCapCoefficient = await contracts.networkController.read.yearlyRewardCapCoefficient({blockNumber: l2blockNumber}); 48 | logger.log(`Yearly Reward Cap Coefficient: ${yearlyRewardCapCoefficient.toString()}`); 49 | 50 | 51 | const apyCap = 52 | (BigInt(10000) * yearlyRewardCapCoefficient * initialRewardPoolsSize) / tvl; 53 | 54 | console.log(`Apy Cap: ${apyCap.toString()}`); 55 | 56 | return apyCap > 2000n ? 2000n : apyCap; 57 | } catch (error) { 58 | console.error('Error calculating APY:', error); 59 | throw error; 60 | } 61 | } 62 | 63 | 64 | (async () => { 65 | console.log(`Latest Distribution: ${await getLatestDistributionBlock()}`) 66 | console.log(`Approve Ranges: ${JSON.stringify(await approveRanges())}`) 67 | console.log(`APY: ${await currentApy(21279057n)}`) 68 | console.log(`APY: ${await currentApy(21279057n)}`) 69 | })().then(() => console.log('Done')) 70 | 71 | -------------------------------------------------------------------------------- /packages/rewards-calculator/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { formatEther } from "viem"; 2 | import bs58 from "bs58"; 3 | 4 | import Decimal from "decimal.js"; 5 | Decimal.set({ precision: 28, minE: -9 }); 6 | 7 | const { decode, encode } = bs58; 8 | export function keysToFixed(object: Object) { 9 | return Object.fromEntries( 10 | Object.entries(object).map(([key, value]) => [ 11 | key, 12 | typeof value === "number" || value instanceof Decimal 13 | ? value.toFixed(2) 14 | : value, 15 | ]), 16 | ); 17 | } 18 | 19 | export function sum(array: number[]) { 20 | return array.reduce((acc, value) => acc + value, 0); 21 | } 22 | 23 | export function bigSum(array: bigint[]) { 24 | return array.reduce((acc, value) => acc + value, 0n); 25 | } 26 | 27 | export function decimalSum(array: Decimal[]) { 28 | return array.reduce((acc, value) => acc.add(value), new Decimal(0)); 29 | } 30 | 31 | export function bigIntToDecimal(value: BigInt) { 32 | return new Decimal(value.toString()); 33 | } 34 | 35 | export function decimalToBigInt(value: Decimal) { 36 | return BigInt(value.round().toFixed(0)); 37 | } 38 | 39 | export function formatSqd(value: Decimal) { 40 | return formatEther(decimalToBigInt(value)).replace(/(\.\d{3})\d+/, "$1"); 41 | } 42 | 43 | export function fromBase58(value: string): `0x${string}` { 44 | return `0x${Buffer.from(decode(value)).toString("hex")}`; 45 | } 46 | 47 | export function toBase58(value: `0x${string}`): string { 48 | return encode(Buffer.from(value.slice(2), "hex")); 49 | } 50 | 51 | export function withCache Promise>(func: T): T { 52 | const cache = new Map>(); 53 | 54 | return (async function (...args: Parameters): Promise> { 55 | // Custom key generator to handle BigInt 56 | const key = args 57 | .map(arg => JSON.stringify(arg, (_, v) => typeof v === 'bigint' ? `bigint:${v.toString()}` : v)) 58 | .join('|'); 59 | 60 | if (cache.has(key)) { 61 | console.log('Returning from cache:', key); 62 | return cache.get(key)!; 63 | } 64 | 65 | const result = await func(...args); 66 | cache.set(key, result); 67 | return result; 68 | }) as T; 69 | } -------------------------------------------------------------------------------- /packages/rewards-calculator/test/data/test_log.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "client_id": "12D3KooWRYijvTzQCZ9UHKYPFQgUGpgkyundiB9vuQXy1n6Bu9TA", 4 | "worker_id": "12D3KooWSyqmbHd1SYNnCNkJzanF37YnYD7zwG8WbqUSpuM15MQ5", 5 | "query_id": "90c419cc-c451-454c-b0e9-3c941b10bf22", 6 | "dataset": "czM6Ly9tb29uYmVhbS1tYWlubmV0", 7 | "query": "{\"fromBlock\":1111111030,\"toBlock\":66666666610000,\"includeAllBlocks\":true}", 8 | "profiling": false, 9 | "client_state_json": "{}", 10 | "query_hash": "c7c3ba6fed5f1a6e2c67fe59f9ff9318fdc7d0ee8ef30d4c4381de588e97b178", 11 | "exec_time_ms": 0, 12 | "result": 2, 13 | "num_read_chunks": 0, 14 | "output_size": 0, 15 | "output_hash": "", 16 | "error_msg": "data for block 1111111030 is not available", 17 | "seq_no": 0, 18 | "client_signature": "ade2a733ca29dcf5b4e8a9de60510ac98856c41b5e149293d95ab7b3773f7a094578f42f8f8ca26f3a85844561c66579cf7b0d1e21b43a8d15dc3240889ec104", 19 | "worker_signature": "d8121b1b867ca02d9083cd99d464784a3b87c3ad68cab768de337b0fa67d2781188cfa296b27097ccd7ea6056939149c6bb5cd8f8044a29c06f88069618aa30c", 20 | "worker_timestamp": 1701873596178, 21 | "collector_timestamp": 1701873646589 22 | }, 23 | { 24 | "client_id": "12D3KooWRYijvTzQCZ9UHKYPFQgUGpgkyundiB9vuQXy1n6Bu9TA", 25 | "worker_id": "12D3KooWSyqmbHd1SYNnCNkJzanF37YnYD7zwG8WbqUSpuM15MQ5", 26 | "query_id": "95609d1f-c8df-4577-bf07-694a33c0f783", 27 | "dataset": "czM6Ly9tb29uYmVhbS1tYWlubmV0", 28 | "query": "{\"fromBlock\":5000,\"toBlock\":10000,\"includeAllBlocks\":true}", 29 | "profiling": false, 30 | "client_state_json": "{}", 31 | "query_hash": "2fc121172864fbf256c97d313ad12df070efc07d755fe63e1f4af58089a231f1", 32 | "exec_time_ms": 71, 33 | "result": 1, 34 | "num_read_chunks": 1, 35 | "output_size": 239447, 36 | "output_hash": "73a4ef37686f48dc9c1ad03469563468bdbbdc3358128e16b8184c0dbf649c99", 37 | "error_msg": "", 38 | "seq_no": 1, 39 | "client_signature": "a378b45bc38ac9fb76980221ac5b8043f14946d166f663e0d6350cb63c7ffe996c8442b05c61c586e7c06659f1fc56dcc4788b21479099f1c2a456dfeda88a0e", 40 | "worker_signature": "10295771f769cb40c4d77bbffd5382360bd3155e714ebf07f2732922db9a016006d1be1d1e7510534c0dbb644a126119aa183e480e7b57ce15a7f2c405753f08", 41 | "worker_timestamp": 1701873615993, 42 | "collector_timestamp": 1701873646589 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /packages/rewards-calculator/test/signature-verification.ts: -------------------------------------------------------------------------------- 1 | import testLog from "./data/test_log.json" assert { type: "json" }; 2 | import { 3 | populateQueryProto, 4 | validateSignatures, 5 | verifySignature, 6 | } from "../src/signatureVerification"; 7 | import { expect } from "chai"; 8 | 9 | const [withDefaultValues, withoutDefaultValues] = testLog; 10 | 11 | describe("Signature verification", () => { 12 | it("populateQueryProto returns buffer", async () => { 13 | const populated = await populateQueryProto(withDefaultValues); 14 | expect(populated).to.be.instanceOf(Uint8Array); 15 | expect(populated.length).to.equal(149); 16 | }); 17 | 18 | it("verifySignature returns true for correct signature in log without default values", async () => { 19 | const message = await populateQueryProto(withoutDefaultValues); 20 | expect( 21 | verifySignature( 22 | message, 23 | withoutDefaultValues.client_signature, 24 | withoutDefaultValues.client_id, 25 | ), 26 | ).to.be.true; 27 | }); 28 | 29 | it("verifySignature returns true for correct signature in withDefaultValues", async () => { 30 | const message = await populateQueryProto(withDefaultValues); 31 | expect( 32 | verifySignature( 33 | message, 34 | withDefaultValues.client_signature, 35 | withDefaultValues.client_id, 36 | ), 37 | ).to.be.true; 38 | }); 39 | 40 | it("verifySignature returns false for incorrect signature", async () => { 41 | const message = await populateQueryProto(withDefaultValues); 42 | expect( 43 | verifySignature( 44 | message, 45 | withDefaultValues.client_signature, 46 | withDefaultValues.worker_id, 47 | ), 48 | ).to.be.false; 49 | }); 50 | 51 | it("verifySignatures returns true for correct query object", async () => { 52 | expect(await validateSignatures(withDefaultValues)).to.be.true; 53 | expect(await validateSignatures(withoutDefaultValues)).to.be.true; 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/rewards-calculator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "preserveConstEnums": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true 14 | }, 15 | "ts-node": { 16 | "esm": true, 17 | "experimentalSpecifierResolution": "node", 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": {}, 5 | "@subsquid-network/reward-stats#build": { 6 | "dependsOn": ["@subsquid-network/contracts#build"] 7 | }, 8 | "@subsquid-network/contract-admin-state#build": { 9 | "dependsOn": ["@subsquid-network/contracts#build"] 10 | }, 11 | "lint": {}, 12 | "test": {} 13 | } 14 | } 15 | --------------------------------------------------------------------------------