├── .solhintignore ├── .eslintignore ├── contracts ├── proxy │ ├── BeaconProxy.sol │ └── UpgradeableBeacon.sol ├── security │ ├── Timelock.sol │ └── PauseControl.sol ├── token │ ├── ISafeERC20.sol │ ├── MockToken.sol │ ├── MockHackToken.sol │ └── StakeToken.sol ├── utils │ ├── DeepCopy.sol │ ├── OnlySender.sol │ ├── TimeInterval.sol │ ├── MarketSpec.sol │ ├── UncheckedMath.sol │ ├── UQ112x112.sol │ ├── DigestHistory.sol │ ├── ZgsSpec.sol │ ├── BlockHash.sol │ ├── Escrow.sol │ ├── PullPayment.sol │ ├── Blake2b.sol │ └── Exponent.sol ├── miner │ ├── WorkerContext.sol │ ├── RecallRange.sol │ └── MineLib.sol ├── test │ ├── DummyMarket.sol │ ├── ExponentTest.sol │ ├── DummyReward.sol │ ├── Blake2bTest.sol │ ├── MerkleTreeTest.sol │ └── PoraMineTest.sol ├── interfaces │ ├── IDigestHistory.sol │ ├── IMarket.sol │ ├── IReward.sol │ ├── IFlow.sol │ └── Submission.sol ├── dataFlow │ └── FixedPriceFlow.sol ├── reward │ ├── ChunkDecayReward.sol │ ├── ChunkLinearReward.sol │ ├── Reward.sol │ └── ChunkRewardBase.sol └── market │ └── FixedPrice.sol ├── .mocharc.json ├── .gitignore ├── src ├── deploy │ ├── deploy_blake2b_test.ts │ ├── deploy_fixed_price_market.ts │ ├── deploy_flow.ts │ ├── deploy_pora_mine.ts │ ├── deploy_pora_mine_test.ts │ ├── deploy_fixed_price_flow.ts │ ├── deploy_chunk_linear_reward.ts │ ├── deploy_no_market_initialization.ts │ └── deploy_market_enabled_initialization.ts ├── networks │ ├── zerog_contract_config.ts │ ├── zerog_testnet_contract_config_turbo.ts │ ├── zerog_mainnet_contract_config_turbo.ts │ ├── zerog_mainnet_contract_config_standard.ts │ └── zerog_testnet_contract_config_standard.ts ├── utils │ ├── constructor_args.ts │ └── utils.ts ├── tasks │ ├── codesize.ts │ ├── mine.ts │ ├── flow.ts │ ├── upgrade.ts │ ├── access.ts │ └── reward.ts ├── config.ts └── dev │ └── view.ts ├── .prettierrc ├── .solhint.json ├── test ├── utils │ ├── deploy.ts │ ├── converts.ts │ ├── snapshot.ts │ ├── params.ts │ └── mockMerkleTree.ts ├── history.spec.ts ├── flow.spec.ts ├── blake2b.spec.ts ├── reward.spec.ts └── merkle.spec.ts ├── .github └── workflows │ └── ci.yml ├── .eslintrc.js ├── patches └── @clrfund+waffle-mock-contract+0.0.11.patch ├── tsconfig.json ├── match.py ├── scripts ├── README.md └── flow-update.sh ├── package.json ├── hardhat.config.ts ├── README.md ├── CONTRACT_PARAMETERS.md └── ROLES_AND_PERMISSIONS.md /.solhintignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/dev -------------------------------------------------------------------------------- /contracts/proxy/BeaconProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; 5 | -------------------------------------------------------------------------------- /contracts/security/Timelock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/governance/TimelockController.sol"; 5 | -------------------------------------------------------------------------------- /contracts/proxy/UpgradeableBeacon.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; 5 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | "ts" 4 | ], 5 | "spec": "./test/**/*.spec.{js,ts}", 6 | "require": ["ts-node/register", "source-map-support/register"], 7 | "timeout": 12000 8 | } -------------------------------------------------------------------------------- /contracts/token/ISafeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 4 | 5 | interface ISafeERC20 is IERC20 {} 6 | -------------------------------------------------------------------------------- /contracts/utils/DeepCopy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8; 3 | 4 | library DeepCopy { 5 | function deepCopy(bytes32[2] memory src) internal pure returns (bytes32[2] memory dst) { 6 | dst[0] = src[0]; 7 | dst[1] = src[1]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/miner/WorkerContext.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../interfaces/IFlow.sol"; 5 | 6 | struct WorkerContext { 7 | MineContext context; 8 | uint poraTarget; 9 | bytes32 subtaskDigest; 10 | uint64 maxShards; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pnp.* 2 | .yarn 3 | .yarnrc 4 | .DS_Store 5 | .openzeppelin 6 | out/ 7 | cache/ 8 | node_modules/ 9 | deployments/ 10 | .env 11 | .idea/ 12 | .vscode/ 13 | /deploy 14 | artifacts/ 15 | typechain-types/ 16 | coverage/ 17 | build/ 18 | coverage.json 19 | typechain-types 20 | abis 21 | yarn-error.log 22 | contract_addresses.txt 23 | -------------------------------------------------------------------------------- /contracts/utils/OnlySender.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import "@openzeppelin/contracts/utils/Context.sol"; 4 | 5 | contract OnlySender is Context { 6 | modifier onlySender(address sender) { 7 | require(_msgSender() == sender, "Sender does not have permission"); 8 | _; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/token/MockToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "./ISafeERC20.sol"; 6 | 7 | contract MockToken is ERC20, ISafeERC20 { 8 | constructor() ERC20("Mock ZeroGStorage Token", "ZGS") { 9 | _mint(msg.sender, 1e9); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/test/DummyMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../interfaces/IReward.sol"; 5 | import "../interfaces/IFlow.sol"; 6 | 7 | contract DummyMarket { 8 | function chargeFee(uint beforeLength, uint uploadSectors, uint paddingSectors) external {} 9 | 10 | function pricePerSector() external pure returns (uint) { 11 | return 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/interfaces/IDigestHistory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | interface IDigestHistory { 5 | function insert(bytes32 data) external returns (uint); 6 | 7 | function available(uint index) external view returns (bool); 8 | 9 | function contains(bytes32 input) external view returns (bool); 10 | 11 | function at(uint index) external view returns (bytes32); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/utils/TimeInterval.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | contract TimeInterval { 5 | uint private lastTimestamp; 6 | 7 | function _tick() internal returns (uint timeElapsed) { 8 | timeElapsed = block.timestamp - lastTimestamp; 9 | lastTimestamp = block.timestamp; 10 | unchecked { 11 | timeElapsed *= 1000; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/test/ExponentTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../utils/Exponent.sol"; 5 | 6 | contract ExponentTest { 7 | function powTwo(uint exponentX64) public pure returns (uint) { 8 | return Exponential.powTwo64X96(exponentX64); 9 | } 10 | 11 | function powHalf(uint exponentX64) public pure returns (uint) { 12 | return Exponential.powHalf64X96(exponentX64); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/test/DummyReward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../interfaces/IMarket.sol"; 5 | import "../interfaces/IReward.sol"; 6 | import "../interfaces/IFlow.sol"; 7 | 8 | contract DummyReward is IReward { 9 | function fillReward(uint beforeLength, uint uploadSectors) external payable {} 10 | 11 | function claimMineReward(uint pricingIndex, address payable beneficiary, bytes32 minerId) external {} 12 | } 13 | -------------------------------------------------------------------------------- /src/deploy/deploy_blake2b_test.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { CONTRACTS, deployDirectly } from "../utils/utils"; 4 | 5 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 6 | await deployDirectly(hre, CONTRACTS.Blake2bTest); 7 | }; 8 | 9 | deploy.tags = [CONTRACTS.Blake2bTest.name, "test"]; 10 | deploy.dependencies = []; 11 | export default deploy; 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | interface IMarket { 5 | function chargeFee(uint beforeLength, uint uploadSectors, uint paddingSectors) external; 6 | 7 | function flow() external view returns (address); 8 | 9 | function pricePerSector() external view returns (uint); 10 | 11 | function reward() external view returns (address); 12 | 13 | function setPricePerSector(uint pricePerSector_) external; 14 | } 15 | -------------------------------------------------------------------------------- /src/deploy/deploy_fixed_price_market.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { CONTRACTS, deployInBeaconProxy } from "../utils/utils"; 4 | 5 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 6 | await deployInBeaconProxy(hre, CONTRACTS.FixedPrice); 7 | }; 8 | 9 | deploy.tags = [CONTRACTS.FixedPrice.name, "market-enabled"]; 10 | deploy.dependencies = []; 11 | export default deploy; 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IReward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity >=0.8.0 <0.9.0; 4 | 5 | interface IReward { 6 | event DistributeReward( 7 | uint indexed pricingIndex, 8 | address indexed beneficiary, 9 | bytes32 indexed minerId, 10 | uint amount 11 | ); 12 | 13 | function fillReward(uint beforeLength, uint rewardSectors) external payable; 14 | 15 | function claimMineReward(uint pricingIndex, address payable beneficiary, bytes32 minerId) external; 16 | } 17 | -------------------------------------------------------------------------------- /contracts/utils/MarketSpec.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "./ZgsSpec.sol"; 5 | 6 | uint constant ANNUAL_ZGS_TOKENS_PER_GB = 10; 7 | uint constant SECOND_ZGS_UNITS_PER_PRICE = (BYTES_PER_PRICE * ANNUAL_ZGS_TOKENS_PER_GB * UNITS_PER_ZGS_TOKEN) / 8 | GB / 9 | SECONDS_PER_YEAR; 10 | 11 | uint constant MONTH_ZGS_UNITS_PER_SECTOR = (BYTES_PER_SECTOR * ANNUAL_ZGS_TOKENS_PER_GB * UNITS_PER_ZGS_TOKEN) / 12 | GB / 13 | MONTH_PER_YEAR; 14 | 15 | uint constant ANNUAL_MILLI_DECAY_RATE = 40; 16 | -------------------------------------------------------------------------------- /contracts/utils/UncheckedMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | library UncheckedMath { 5 | function add(uint a, uint b) internal pure returns (uint) { 6 | unchecked { 7 | return a + b; 8 | } 9 | } 10 | 11 | function sub(uint a, uint b) internal pure returns (uint) { 12 | unchecked { 13 | return a - b; 14 | } 15 | } 16 | 17 | function mul(uint a, uint b) internal pure returns (uint) { 18 | unchecked { 19 | return a * b; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/deploy/deploy_flow.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConstructorArgs } from "../utils/constructor_args"; 4 | import { CONTRACTS, deployInBeaconProxy } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | await deployInBeaconProxy(hre, CONTRACTS.Flow, getConstructorArgs(hre.network.name, CONTRACTS.Flow.name)); 8 | }; 9 | 10 | deploy.tags = [CONTRACTS.Flow.name, "no-market"]; 11 | deploy.dependencies = []; 12 | export default deploy; 13 | -------------------------------------------------------------------------------- /src/deploy/deploy_pora_mine.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConstructorArgs } from "../utils/constructor_args"; 4 | import { CONTRACTS, deployInBeaconProxy } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | await deployInBeaconProxy(hre, CONTRACTS.PoraMine, getConstructorArgs(hre.network.name, CONTRACTS.PoraMine.name)); 8 | }; 9 | 10 | deploy.tags = [CONTRACTS.PoraMine.name, "market-enabled"]; 11 | deploy.dependencies = []; 12 | export default deploy; 13 | -------------------------------------------------------------------------------- /src/deploy/deploy_pora_mine_test.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConstructorArgs } from "../utils/constructor_args"; 4 | import { CONTRACTS, deployInBeaconProxy } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | await deployInBeaconProxy( 8 | hre, 9 | CONTRACTS.PoraMineTest, 10 | getConstructorArgs(hre.network.name, CONTRACTS.PoraMineTest.name) 11 | ); 12 | }; 13 | 14 | deploy.tags = [CONTRACTS.PoraMineTest.name, "no-market"]; 15 | deploy.dependencies = []; 16 | export default deploy; 17 | -------------------------------------------------------------------------------- /src/deploy/deploy_fixed_price_flow.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConstructorArgs } from "../utils/constructor_args"; 4 | import { CONTRACTS, deployInBeaconProxy } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | await deployInBeaconProxy( 8 | hre, 9 | CONTRACTS.FixedPriceFlow, 10 | getConstructorArgs(hre.network.name, CONTRACTS.FixedPriceFlow.name) 11 | ); 12 | }; 13 | 14 | deploy.tags = [CONTRACTS.FixedPriceFlow.name, "market-enabled"]; 15 | deploy.dependencies = []; 16 | export default deploy; 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-solidity"], 5 | "organizeImportsSkipDestructiveCodeActions": true, 6 | "overrides": [ 7 | { 8 | "files": "*.sol", 9 | "options": { 10 | "printWidth": 120, 11 | "tabWidth": 4, 12 | "useTabs": false, 13 | "singleQuote": false, 14 | "bracketSpacing": false 15 | } 16 | }, 17 | { 18 | "files": "*.ts", 19 | "options": { 20 | "printWidth": 120 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/deploy/deploy_chunk_linear_reward.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConstructorArgs } from "../utils/constructor_args"; 4 | import { CONTRACTS, deployInBeaconProxy } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | await deployInBeaconProxy( 8 | hre, 9 | CONTRACTS.ChunkLinearReward, 10 | getConstructorArgs(hre.network.name, CONTRACTS.ChunkLinearReward.name) 11 | ); 12 | }; 13 | 14 | deploy.tags = [CONTRACTS.ChunkLinearReward.name, "market-enabled"]; 15 | deploy.dependencies = []; 16 | export default deploy; 17 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "avoid-low-level-calls": "off", 6 | "compiler-version": "off", 7 | "gas-custom-errors": "off", 8 | "explicit-types": ["warn", "implicit"], 9 | "func-visibility": ["warn", { "ignoreConstructors": true }], 10 | "max-states-count": "off", 11 | "no-empty-blocks": "off", 12 | "no-global-import": "off", 13 | "no-inline-assembly": "off", 14 | "not-rely-on-time": "off", 15 | "prettier/prettier": "error", 16 | "reason-string": "off", 17 | "immutable-vars-naming": "off", 18 | "one-contract-per-file": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/utils/UQ112x112.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 5 | 6 | // range: [0, 2**112 - 1] 7 | // resolution: 1 / 2**112 8 | 9 | library UQ112x112 { 10 | uint224 public constant Q112 = 2 ** 112; 11 | 12 | // encode a uint112 as a UQ112x112 13 | function encode(uint112 y) internal pure returns (uint224 z) { 14 | z = uint224(y) * Q112; // never overflows 15 | } 16 | 17 | // divide a UQ112x112 by a uint112, returning a UQ112x112 18 | function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { 19 | z = x / uint224(y); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/security/PauseControl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity ^0.8.2; 4 | 5 | import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; 6 | import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; 7 | 8 | contract PauseControl is PausableUpgradeable, AccessControlEnumerableUpgradeable { 9 | // role 10 | bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); 11 | 12 | function pause() external onlyRole(PAUSER_ROLE) { 13 | _pause(); 14 | } 15 | 16 | function unpause() external onlyRole(PAUSER_ROLE) { 17 | _unpause(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/dataFlow/FixedPriceFlow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import "./Flow.sol"; 4 | import "../market/FixedPrice.sol"; 5 | 6 | contract FixedPriceFlow is Flow { 7 | error NotEnoughFee(uint price, uint amount, uint paid); 8 | 9 | constructor(uint deployDelay_) Flow(deployDelay_) {} 10 | 11 | function _beforeSubmit(uint sectors) internal override { 12 | uint price = FixedPrice(market()).pricePerSector(); 13 | uint fee = sectors * price; 14 | uint paid = address(this).balance; 15 | 16 | if (fee > address(this).balance) { 17 | revert NotEnoughFee(price, sectors, paid); 18 | } 19 | 20 | payable(address(market())).transfer(fee); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/utils/deploy.ts: -------------------------------------------------------------------------------- 1 | import { deployMockContract, MockContract } from "@clrfund/waffle-mock-contract"; 2 | import { parseEther, Signer } from "ethers"; 3 | import env = require("hardhat"); 4 | 5 | async function deployMock(owner: Signer, name: string): Promise { 6 | const abi = (await env.artifacts.readArtifact(name)).abi; 7 | return await deployMockContract(owner, abi); 8 | } 9 | 10 | async function transferBalance(owner: Signer, receiver: string, value: string) { 11 | const amount = parseEther(value); 12 | 13 | const tx = await owner.sendTransaction({ 14 | to: receiver, 15 | value: amount, 16 | gasLimit: 210000, 17 | }); 18 | 19 | await tx.wait(); 20 | } 21 | 22 | export { deployMock, transferBalance }; 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Solidity CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | - next_mine_spec 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '18.17' 24 | cache: 'yarn' 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Run lint 30 | run: yarn lint 31 | 32 | - name: Build 33 | run: yarn build 34 | 35 | - name: Run tests 36 | run: yarn test 37 | -------------------------------------------------------------------------------- /src/networks/zerog_contract_config.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { NetworkConfigs } from "../config"; 3 | 4 | export const ZerogContractConfigs: NetworkConfigs = { 5 | mineConfigs: { 6 | settings: 0, 7 | initDifficulty: 180000, 8 | targetMineBlocks: 600, 9 | targetSubmissions: 5, 10 | maxShards: 32, 11 | nSubtasks: 1, 12 | subtaskInterval: 600, // Sequential, no overlap 13 | }, 14 | chunkRewardConfigs: { 15 | foundationAdmin: "0x711b0EcB072C27DE0e50c9944d7195A51B202522", // Foundation admin address 16 | }, 17 | blocksPerEpoch: 1200, 18 | firstBlock: 0, 19 | rootHistory: ZeroAddress, 20 | lifetimeMonth: 3, 21 | flowDeployDelay: 0, 22 | unitPrice: 1, 23 | }; 24 | -------------------------------------------------------------------------------- /contracts/test/Blake2bTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../utils/Blake2b.sol"; 5 | 6 | contract Blake2bTest { 7 | function blake2bPair(bytes32[2] memory input) external view returns (bytes32[2] memory h) { 8 | return Blake2b.blake2b(input); 9 | } 10 | 11 | function blake2bTriple(bytes32[3] memory input) external view returns (bytes32[2] memory h) { 12 | return Blake2b.blake2b(input); 13 | } 14 | 15 | function blake2bFive(bytes32[5] memory input) external view returns (bytes32[2] memory h) { 16 | return Blake2b.blake2b(input); 17 | } 18 | 19 | function blake2b(bytes32[] memory input) external view returns (bytes32[2] memory h) { 20 | return Blake2b.blake2b(input); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/reward/ChunkDecayReward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity >=0.8.0 <0.9.0; 4 | 5 | import "./Reward.sol"; 6 | import "./ChunkRewardBase.sol"; 7 | import "../utils/MarketSpec.sol"; 8 | 9 | contract ChunkDecayReward is ChunkRewardBase { 10 | using RewardLibrary for Reward; 11 | uint16 public immutable annualMilliDecayRate; 12 | 13 | constructor(uint16 annualMilliDecayRate_) { 14 | annualMilliDecayRate = annualMilliDecayRate_; 15 | } 16 | 17 | function _releasedReward(Reward memory reward) internal view override returns (uint) { 18 | return reward.expDecayReward(annualMilliDecayRate); 19 | } 20 | 21 | function _baseReward(uint, Reward memory, uint) internal view override returns (uint) { 22 | return baseReward(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/utils/converts.ts: -------------------------------------------------------------------------------- 1 | import { AbiCoder } from "ethers"; 2 | 3 | const abiCoder = new AbiCoder(); 4 | 5 | function hexToBuffer(hex: string): Buffer { 6 | if (hex.slice(0, 2) === "0x") { 7 | hex = hex.slice(2); 8 | } 9 | return Buffer.from(hex, "hex"); 10 | } 11 | 12 | function bufferToBigInt(buffer: Buffer): bigint { 13 | // Ensure the Buffer is in Big Endian format 14 | let bigIntValue = BigInt(0); 15 | 16 | // Iterate over each byte of the buffer 17 | for (let i = 0; i < buffer.length; i++) { 18 | // Shift the current value to the left (by 8 bits for each byte) 19 | bigIntValue = (bigIntValue << BigInt(8)) | BigInt(buffer[i]); 20 | } 21 | 22 | return bigIntValue; 23 | } 24 | 25 | function numToU256(num: number): Buffer { 26 | return hexToBuffer(abiCoder.encode(["uint256"], [num])); 27 | } 28 | 29 | export { hexToBuffer, bufferToBigInt, numToU256 }; 30 | -------------------------------------------------------------------------------- /contracts/test/MerkleTreeTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import "../dataFlow/FlowTreeLib.sol"; 4 | 5 | contract MerkleTreeTest { 6 | using FlowTreeLib for FlowTree; 7 | FlowTree public tree; 8 | 9 | constructor() { 10 | tree.initialize(bytes32(0x0)); 11 | } 12 | 13 | function insertNode(bytes32 nodeRoot, uint height) external returns (uint) { 14 | return tree.insertNode(nodeRoot, height); 15 | } 16 | 17 | function root() external view returns (bytes32) { 18 | return tree.root(); 19 | } 20 | 21 | function commitRoot() external { 22 | tree.commitRoot(); 23 | } 24 | 25 | function currentLength() external view returns (uint) { 26 | return tree.currentLength; 27 | } 28 | 29 | function unstagedHeight() external view returns (uint) { 30 | return tree.unstagedHeight; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/token/MockHackToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "./ISafeERC20.sol"; 6 | 7 | // In previous, zgsToken is an ERC20 token. However, it becomes a native token now. 8 | // This is a hotfix contract for making existing test code be compatible. 9 | // It is not intended for more usage. 10 | contract MockHackToken is ERC20, ISafeERC20 { 11 | constructor() ERC20("Mock ZeroGStorage Token", "ZGS") { 12 | _mint(msg.sender, 1000); 13 | } 14 | 15 | function _update(address from, address to, uint amount) internal override { 16 | super._update(from, to, amount); 17 | if (to == address(0)) { 18 | return; 19 | } 20 | _burn(to, amount); 21 | payable(to).transfer(amount); 22 | } 23 | 24 | function receiveNative() external payable {} 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/strict-type-checked", 10 | "plugin:prettier/recommended", 11 | ], 12 | overrides: [], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | project: true, 17 | sourceType: "module", 18 | }, 19 | plugins: ["@typescript-eslint", "no-only-tests"], 20 | rules: { 21 | eqeqeq: "error", 22 | "@typescript-eslint/consistent-type-imports": [ 23 | "error", 24 | { prefer: "no-type-imports" }, 25 | ], 26 | '@typescript-eslint/no-unsafe-call': 'off', 27 | '@typescript-eslint/no-unsafe-assignment': 'off', 28 | '@typescript-eslint/no-unsafe-member-access': 'off', 29 | '@typescript-eslint/no-unsafe-argument': 'off', 30 | }, 31 | ignorePatterns: [".eslintrc.js"], 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/constructor_args.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from "../config"; 2 | import { CONTRACTS } from "./utils"; 3 | 4 | export function getConstructorArgs(network: string, name: string): unknown[] { 5 | const config = getConfig(network); 6 | let args: unknown[] = []; 7 | switch (name) { 8 | case CONTRACTS.ChunkLinearReward.name: { 9 | args = [config.lifetimeMonth * 31 * 86400]; 10 | break; 11 | } 12 | case CONTRACTS.FixedPriceFlow.name: { 13 | args = [config.flowDeployDelay]; 14 | break; 15 | } 16 | case CONTRACTS.Flow.name: { 17 | args = [config.flowDeployDelay]; 18 | break; 19 | } 20 | case CONTRACTS.PoraMine.name: { 21 | args = [config.mineConfigs.settings]; 22 | break; 23 | } 24 | case CONTRACTS.PoraMineTest.name: { 25 | args = [config.mineConfigs.settings]; 26 | break; 27 | } 28 | default: { 29 | break; 30 | } 31 | } 32 | return args; 33 | } 34 | -------------------------------------------------------------------------------- /src/tasks/codesize.ts: -------------------------------------------------------------------------------- 1 | import { task, types } from "hardhat/config"; 2 | 3 | task("codesize", "show codesize of the contracts") 4 | .addParam("contractname", "contract name", undefined, types.string, true) 5 | .setAction(async (taskArgs: { contractname: string }, hre) => { 6 | const contracts = await hre.artifacts.getAllFullyQualifiedNames(); 7 | const ans = []; 8 | for (const contract of contracts) { 9 | const artifact = await hre.artifacts.readArtifact(contract); 10 | if (taskArgs.contractname && taskArgs.contractname !== artifact.contractName) continue; 11 | ans.push([ 12 | artifact.contractName, 13 | Math.max(0, (artifact.deployedBytecode.length - 2) / 2), 14 | "bytes (max 24576)", 15 | ]); 16 | } 17 | ans.sort((a, b) => { 18 | if (a[1] > b[1]) return -1; 19 | if (a[1] < b[1]) return 1; 20 | return 0; 21 | }); 22 | for (const x of ans) { 23 | if (Number(x[1]) > 0) console.log(`${x[0]}:\t${x[1]} ${x[2]}`); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/networks/zerog_testnet_contract_config_turbo.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { NetworkConfigs } from "../config"; 3 | 4 | export const ZerogTestnetContractConfigsTurbo: NetworkConfigs = { 5 | mineConfigs: { 6 | settings: 0, // | `settings` | `uint` | Bit flags for mining features | Bitwise flags: NO_DATA_SEAL (0x1), NO_DATA_PROOF (0x2), FIXED_DIFFICULTY (0x4) | No | 7 | initDifficulty: 1000000, 8 | targetMineBlocks: 750, // 5min = 300s, with 0.4s per block 9 | targetSubmissions: 100, // n x 8GB > throughput GB/s * epoch window = 25 min, n/nSubtasks * nOverlapSubtasks * poraGas < tps (166) * 21000 * targetMineBlock 10 | maxShards: 256, 11 | nSubtasks: 4, 12 | subtaskInterval: 800, // non-overlap, (0, 750], (800, 1550],... 13 | }, 14 | chunkRewardConfigs: { 15 | foundationAdmin: "0x711b0EcB072C27DE0e50c9944d7195A51B202522", // Foundation admin address 16 | }, 17 | blocksPerEpoch: 3750, 18 | firstBlock: 1, 19 | rootHistory: ZeroAddress, 20 | lifetimeMonth: 12, 21 | flowDeployDelay: 0, 22 | unitPrice: 33, 23 | }; 24 | -------------------------------------------------------------------------------- /test/utils/snapshot.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 3 | import hre = require("hardhat"); 4 | 5 | class Snapshot { 6 | snapshotId; 7 | constructor() { 8 | this.snapshotId = ""; 9 | } 10 | 11 | async snapshot(): Promise { 12 | this.snapshotId = await hre.network.provider.send("evm_snapshot", []); 13 | return this; 14 | } 15 | 16 | async revert() { 17 | await hre.network.provider.send("evm_revert", [this.snapshotId]); 18 | await this.snapshot(); 19 | } 20 | } 21 | 22 | async function increaseTime(seconds: number): Promise { 23 | return await hre.network.provider.send("evm_increaseTime", [seconds]); 24 | } 25 | 26 | async function enableAutomine(): Promise { 27 | return await hre.network.provider.send("evm_setAutomine", [true]); 28 | } 29 | 30 | async function disableAutomine(): Promise { 31 | return await hre.network.provider.send("evm_setAutomine", [false]); 32 | } 33 | 34 | export { Snapshot, increaseTime, enableAutomine, disableAutomine }; 35 | -------------------------------------------------------------------------------- /src/networks/zerog_mainnet_contract_config_turbo.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { NetworkConfigs } from "../config"; 3 | 4 | export const ZerogMainnetContractConfigsTurbo: NetworkConfigs = { 5 | mineConfigs: { 6 | settings: 0, // | `settings` | `uint` | Bit flags for mining features | Bitwise flags: NO_DATA_SEAL (0x1), NO_DATA_PROOF (0x2), FIXED_DIFFICULTY (0x4) | No | 7 | initDifficulty: 1000000, 8 | targetMineBlocks: 750, // 5min = 300s, with 0.4s per block 9 | targetSubmissions: 100, // n x 8GB > throughput GB/s * epoch window = 25 min, n/nSubtasks * nOverlapSubtasks * poraGas < tps (166) * 21000 * targetMineBlock 10 | maxShards: 256, 11 | nSubtasks: 4, 12 | subtaskInterval: 800, // non-overlap, (0, 750], (800, 1550],... 13 | }, 14 | chunkRewardConfigs: { 15 | foundationAdmin: "0x711b0EcB072C27DE0e50c9944d7195A51B202522", // Foundation admin address 16 | }, 17 | blocksPerEpoch: 3750, 18 | firstBlock: 2386981, 19 | rootHistory: ZeroAddress, 20 | lifetimeMonth: 12, 21 | flowDeployDelay: 0, 22 | unitPrice: 33, 23 | }; 24 | -------------------------------------------------------------------------------- /src/networks/zerog_mainnet_contract_config_standard.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { NetworkConfigs } from "../config"; 3 | 4 | export const ZerogMainnetContractConfigsStandard: NetworkConfigs = { 5 | mineConfigs: { 6 | settings: 0, // | `settings` | `uint` | Bit flags for mining features | Bitwise flags: NO_DATA_SEAL (0x1), NO_DATA_PROOF (0x2), FIXED_DIFFICULTY (0x4) | No | 7 | initDifficulty: 1000000, 8 | targetMineBlocks: 750, // 5min = 300s, with 0.4s per block 9 | targetSubmissions: 100, // n x 8GB > throughput GB/s * epoch window = 25 min, n/nSubtasks * nOverlapSubtasks * poraGas < tps (166) * 21000 * targetMineBlock 10 | maxShards: 256, 11 | nSubtasks: 4, 12 | subtaskInterval: 800, // non-overlap, (0, 750], (800, 1550],... 13 | }, 14 | chunkRewardConfigs: { 15 | foundationAdmin: "0x711b0EcB072C27DE0e50c9944d7195A51B202522", // Foundation admin address 16 | }, 17 | blocksPerEpoch: 3750, 18 | firstBlock: 10566563, 19 | rootHistory: ZeroAddress, 20 | lifetimeMonth: 12, 21 | flowDeployDelay: 0, 22 | unitPrice: 24, 23 | }; 24 | -------------------------------------------------------------------------------- /src/networks/zerog_testnet_contract_config_standard.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { NetworkConfigs } from "../config"; 3 | 4 | export const ZerogTestnetContractConfigsStandard: NetworkConfigs = { 5 | mineConfigs: { 6 | settings: 0, // | `settings` | `uint` | Bit flags for mining features | Bitwise flags: NO_DATA_SEAL (0x1), NO_DATA_PROOF (0x2), FIXED_DIFFICULTY (0x4) | No | 7 | initDifficulty: 1000000, 8 | targetMineBlocks: 750, // 5min = 300s, with 0.4s per block 9 | targetSubmissions: 100, // n x 8GB > throughput GB/s * epoch window = 25 min, n/nSubtasks * nOverlapSubtasks * poraGas < tps (166) * 21000 * targetMineBlock 10 | maxShards: 256, 11 | nSubtasks: 4, 12 | subtaskInterval: 800, // non-overlap, (0, 750], (800, 1550],... 13 | }, 14 | chunkRewardConfigs: { 15 | foundationAdmin: "0x711b0EcB072C27DE0e50c9944d7195A51B202522", // Foundation admin address 16 | }, 17 | blocksPerEpoch: 3750, 18 | firstBlock: 3827259, 19 | rootHistory: ZeroAddress, 20 | lifetimeMonth: 12, 21 | flowDeployDelay: 0, 22 | unitPrice: 24, 23 | }; 24 | -------------------------------------------------------------------------------- /contracts/test/PoraMineTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../miner/Mine.sol"; 5 | import "../miner/MineLib.sol"; 6 | 7 | contract PoraMineTest is PoraMine { 8 | // 1, 1, settings | 0x4 9 | constructor(uint settings) PoraMine(settings | 0x4) {} 10 | 11 | function setMiner(bytes32 minerId) external { 12 | _setMiner(minerId); 13 | } 14 | 15 | function setQuality(uint _targetQuality) external { 16 | _setQuality(_targetQuality); 17 | } 18 | 19 | function unseal( 20 | MineLib.PoraAnswer memory answer 21 | ) external pure returns (bytes32[UNITS_PER_SEAL] memory unsealedData) { 22 | return MineLib.unseal(answer); 23 | } 24 | 25 | function recoverMerkleRoot( 26 | MineLib.PoraAnswer memory answer, 27 | bytes32[UNITS_PER_SEAL] memory unsealedData 28 | ) external pure returns (bytes32) { 29 | return MineLib.recoverMerkleRoot(answer, unsealedData); 30 | } 31 | 32 | function testAll(MineLib.PoraAnswer memory answer, bytes32 subtaskDigest) external view { 33 | bytes32[UNITS_PER_SEAL] memory unsealedData = MineLib.unseal(answer); 34 | 35 | MineLib.recoverMerkleRoot(answer, unsealedData); 36 | delete unsealedData; 37 | 38 | pora(answer, subtaskDigest); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/token/StakeToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "./ISafeERC20.sol"; 6 | 7 | contract StakeToken is ERC20, ISafeERC20 { 8 | constructor() ERC20("Staked ZeroGStorage", "sZGS") {} 9 | 10 | event Stake(address indexed sender, uint zgsAmount, uint stakeAmount); 11 | event Unstake(address indexed sender, uint zgsAmount, uint stakeAmount); 12 | 13 | ISafeERC20 public zgsToken; 14 | 15 | function stake(uint depositAmount) public { 16 | uint zgsBalance = zgsToken.balanceOf(address(this)); 17 | zgsToken.transferFrom(msg.sender, address(this), depositAmount); 18 | 19 | uint supply = totalSupply(); 20 | uint mintAmount = (depositAmount * supply) / zgsBalance; 21 | 22 | _mint(msg.sender, mintAmount); 23 | emit Stake(msg.sender, depositAmount, mintAmount); 24 | } 25 | 26 | function unstake(uint burnAmount) public { 27 | uint zgsBalance = zgsToken.balanceOf(address(this)); 28 | 29 | uint supply = totalSupply(); 30 | uint claimAmount = (burnAmount * zgsBalance) / supply; 31 | 32 | zgsToken.transfer(msg.sender, burnAmount); 33 | _burn(msg.sender, burnAmount); 34 | emit Unstake(msg.sender, claimAmount, burnAmount); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /patches/@clrfund+waffle-mock-contract+0.0.11.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@clrfund/waffle-mock-contract/dist/cjs/src/index.js b/node_modules/@clrfund/waffle-mock-contract/dist/cjs/src/index.js 2 | index 629a8c5..fc90c37 100644 3 | --- a/node_modules/@clrfund/waffle-mock-contract/dist/cjs/src/index.js 4 | +++ b/node_modules/@clrfund/waffle-mock-contract/dist/cjs/src/index.js 5 | @@ -15,11 +15,13 @@ class Stub { 6 | this.revertSet = false; 7 | this.argsSet = false; 8 | this.callData = func.selector; 9 | + this.callDataPlain = func.selector; 10 | } 11 | err(reason) { 12 | this.stubCalls = []; 13 | this.revertSet = false; 14 | this.argsSet = false; 15 | + this.callData = this.callDataPlain; 16 | throw new Error(reason); 17 | } 18 | returns(...args) { 19 | @@ -91,6 +93,7 @@ class Stub { 20 | this.stubCalls = []; 21 | this.argsSet = false; 22 | this.revertSet = false; 23 | + this.callData = this.callDataPlain; 24 | reject(e); 25 | return; 26 | } 27 | @@ -98,6 +101,7 @@ class Stub { 28 | this.stubCalls = []; 29 | this.argsSet = false; 30 | this.revertSet = false; 31 | + this.callData = this.callDataPlain; 32 | resolve(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/utils/params.ts: -------------------------------------------------------------------------------- 1 | const BYTES_PER_SECTOR = 256; 2 | const BYTES_PER_SEAL = 4 * 1024; // 4 KB 3 | const BYTES_PER_PAD = 16 * 1024; // 16 KB 4 | const BYTES_PER_LOAD = 256 * 1024; // 256 KB 5 | const BYTES_PER_PRICE = 8 * 1024 * 1024 * 1024; // 8 GB 6 | const BYTES_PER_SEGMENT = 256 * 1024; // 256 KB 7 | 8 | const BYTES_PER_UNIT = 32; 9 | const BYTES_PER_BHASH = 64; 10 | 11 | const UNITS_PER_SECTOR = BYTES_PER_SECTOR / BYTES_PER_UNIT; 12 | const UNITS_PER_SEAL = BYTES_PER_SEAL / BYTES_PER_UNIT; 13 | 14 | const SECTORS_PER_SEAL = BYTES_PER_SEAL / BYTES_PER_SECTOR; 15 | const SECTORS_PER_PRICE = BYTES_PER_PRICE / BYTES_PER_SECTOR; 16 | const SECTORS_PER_LOAD = BYTES_PER_LOAD / BYTES_PER_SECTOR; 17 | const SECTORS_PER_SEGMENT = BYTES_PER_SEGMENT / BYTES_PER_SECTOR; 18 | 19 | const SEALS_PER_LOAD = BYTES_PER_LOAD / BYTES_PER_SEAL; 20 | const PADS_PER_LOAD = BYTES_PER_LOAD / BYTES_PER_PAD; 21 | 22 | const SEALS_PER_PAD = SEALS_PER_LOAD / PADS_PER_LOAD; 23 | const BHASHES_PER_SEAL = BYTES_PER_SEAL / BYTES_PER_BHASH; 24 | const BHASHES_PER_PAD = BYTES_PER_PAD / BYTES_PER_BHASH; 25 | 26 | export { 27 | BYTES_PER_SECTOR, 28 | BYTES_PER_SEAL, 29 | BYTES_PER_PAD, 30 | BYTES_PER_LOAD, 31 | BYTES_PER_PRICE, 32 | BYTES_PER_SEGMENT, 33 | BYTES_PER_UNIT, 34 | BYTES_PER_BHASH, 35 | UNITS_PER_SECTOR, 36 | UNITS_PER_SEAL, 37 | SECTORS_PER_SEAL, 38 | SECTORS_PER_PRICE, 39 | SECTORS_PER_LOAD, 40 | SECTORS_PER_SEGMENT, 41 | SEALS_PER_LOAD, 42 | PADS_PER_LOAD, 43 | SEALS_PER_PAD, 44 | BHASHES_PER_SEAL, 45 | BHASHES_PER_PAD, 46 | }; 47 | -------------------------------------------------------------------------------- /contracts/utils/DigestHistory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "../interfaces/IDigestHistory.sol"; 6 | 7 | contract DigestHistory is IDigestHistory, Ownable { 8 | bytes32[] private digests; 9 | uint private nextIndex; 10 | 11 | error UnavailableIndex(uint); 12 | 13 | constructor(uint capacity) Ownable(msg.sender) { 14 | digests = new bytes32[](capacity); 15 | nextIndex = 0; 16 | } 17 | 18 | function insert(bytes32 data) external onlyOwner returns (uint) { 19 | uint index = nextIndex; 20 | uint slot = nextIndex % digests.length; 21 | digests[slot] = data; 22 | nextIndex += 1; 23 | return index; 24 | } 25 | 26 | function available(uint index) public view returns (bool) { 27 | uint capacity = digests.length; 28 | return index < nextIndex && index >= Math.max(nextIndex, capacity) - capacity; 29 | } 30 | 31 | function contains(bytes32 input) external view returns (bool) { 32 | uint maxIndex = Math.min(nextIndex, digests.length); 33 | for (uint i = 0; i < maxIndex; i++) { 34 | if (digests[i] == input) { 35 | return true; 36 | } 37 | } 38 | return false; 39 | } 40 | 41 | function at(uint index) external view returns (bytes32) { 42 | if (!available(index)) { 43 | revert UnavailableIndex(index); 44 | } 45 | return digests[index % digests.length]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/tasks/mine.ts: -------------------------------------------------------------------------------- 1 | import { task, types } from "hardhat/config"; 2 | import { CONTRACTS, getTypedContract } from "../utils/utils"; 3 | 4 | task("mine:show", "show contract params").setAction(async (_, hre) => { 5 | const mine = await getTypedContract(hre, CONTRACTS.PoraMine); 6 | console.log(await mine.targetSubmissions()); 7 | console.log(await mine.canSubmit.staticCall()); 8 | }); 9 | 10 | task("mine:setTargetSubmissions", "set target submissions") 11 | .addParam("n", "number of target submissions", undefined, types.int, false) 12 | .setAction(async (taskArgs: { n: number }, hre) => { 13 | const mine = await getTypedContract(hre, CONTRACTS.PoraMine); 14 | await (await mine.setTargetSubmissions(taskArgs.n)).wait(); 15 | console.log(`set target submission to ${taskArgs.n}`); 16 | }); 17 | 18 | task("mine:setMinDifficulty", "set min difficulty") 19 | .addParam("min", "number of min difficulty", undefined, types.bigint, false) 20 | .setAction(async (taskArgs: { min: bigint }, hre) => { 21 | const mine = await getTypedContract(hre, CONTRACTS.PoraMine); 22 | await (await mine.setMinDifficulty(taskArgs.min)).wait(); 23 | console.log(`set min difficulty to ${taskArgs.min}`); 24 | }); 25 | 26 | task("mine:setNumSubtasks", "set num subtasks") 27 | .addParam("n", "number of num subtasks", undefined, types.int, false) 28 | .setAction(async (taskArgs: { n: number }, hre) => { 29 | const mine = await getTypedContract(hre, CONTRACTS.PoraMine); 30 | await (await mine.setNumSubtasks(taskArgs.n)).wait(); 31 | console.log(`set num subtasks to ${taskArgs.n}`); 32 | }); 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 5 | "lib": ["ES2015"], 6 | "allowJs": false /* Allow javascript files to be compiled. */, 7 | "declaration": true /* Generates corresponding '.d.ts' file. */, 8 | "sourceMap": false /* Generates corresponding '.map' file. */, 9 | "outDir": "./dist" /* Redirect output structure to the directory. */, 10 | "strict": true /* Enable all strict type-checking options. */, 11 | "noUnusedParameters": true /* Report errors on unused parameters. */, 12 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 13 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 14 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 15 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 16 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 17 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 18 | "resolveJsonModule": true 19 | }, 20 | "exclude": ["node_modules"], 21 | "include": ["./**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /contracts/utils/ZgsSpec.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | uint constant KB = 1024; 5 | uint constant MB = 1024 * KB; 6 | uint constant GB = 1024 * MB; 7 | uint constant TB = 1024 * GB; 8 | 9 | uint constant MAX_MINING_LENGTH = (8 * TB) / BYTES_PER_SECTOR; 10 | 11 | uint constant BYTES_PER_SECTOR = 256; 12 | uint constant BYTES_PER_SEAL = 4 * KB; 13 | uint constant BYTES_PER_PAD = 16 * KB; 14 | uint constant BYTES_PER_LOAD = 256 * KB; 15 | uint constant BYTES_PER_PRICE = 8 * GB; 16 | uint constant BYTES_PER_SEGMENT = 256 * KB; 17 | 18 | uint constant BYTES_PER_UNIT = 32; 19 | uint constant BYTES_PER_BHASH = 64; 20 | 21 | uint constant UNITS_PER_SECTOR = BYTES_PER_SECTOR / BYTES_PER_UNIT; 22 | uint constant UNITS_PER_SEAL = BYTES_PER_SEAL / BYTES_PER_UNIT; 23 | 24 | uint constant SECTORS_PER_SEAL = BYTES_PER_SEAL / BYTES_PER_SECTOR; 25 | uint constant SECTORS_PER_PRICE = BYTES_PER_PRICE / BYTES_PER_SECTOR; 26 | uint constant SECTORS_PER_LOAD = BYTES_PER_LOAD / BYTES_PER_SECTOR; 27 | uint constant SECTORS_PER_SEGMENT = BYTES_PER_SEGMENT / BYTES_PER_SECTOR; 28 | 29 | uint constant SEALS_PER_LOAD = BYTES_PER_LOAD / BYTES_PER_SEAL; 30 | uint constant PADS_PER_LOAD = BYTES_PER_LOAD / BYTES_PER_PAD; 31 | 32 | uint constant SEALS_PER_PAD = SEALS_PER_LOAD / PADS_PER_LOAD; 33 | uint constant BHASHES_PER_SEAL = BYTES_PER_SEAL / BYTES_PER_BHASH; 34 | 35 | uint constant UNITS_PER_ZGS_TOKEN = 1_000_000_000_000_000_000; 36 | 37 | uint constant DAYS_PER_MONTH = 31; 38 | uint constant SECONDS_PER_MONTH = 86400 * DAYS_PER_MONTH; 39 | 40 | uint constant MONTH_PER_YEAR = 12; 41 | uint constant DAYS_PER_YEAR = MONTH_PER_YEAR * DAYS_PER_MONTH; 42 | uint constant SECONDS_PER_YEAR = 86400 * DAYS_PER_YEAR; 43 | uint constant MILLI_SECONDS_PER_YEAR = 1000 * SECONDS_PER_YEAR; 44 | -------------------------------------------------------------------------------- /src/deploy/deploy_no_market_initialization.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConfig } from "../config"; 4 | import { CONTRACTS, deployDirectly, getTypedContract } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const config = getConfig(hre.network.name); 8 | // deploy dummy contracts 9 | console.log(`deploying dummy market and reward..`); 10 | await deployDirectly(hre, CONTRACTS.DummyMarket); 11 | await deployDirectly(hre, CONTRACTS.DummyReward); 12 | // initialize all contracts 13 | const poraMineTest_ = await getTypedContract(hre, CONTRACTS.PoraMineTest); 14 | const flow_ = await getTypedContract(hre, CONTRACTS.Flow); 15 | const dummyMarket_ = await getTypedContract(hre, CONTRACTS.DummyMarket); 16 | const dummyReward_ = await getTypedContract(hre, CONTRACTS.DummyReward); 17 | 18 | console.log(`initializing pora mine test..`); 19 | await ( 20 | await poraMineTest_.initialize(await flow_.getAddress(), await dummyReward_.getAddress(), { 21 | difficulty: config.mineConfigs.initDifficulty, 22 | targetMineBlocks: config.mineConfigs.targetMineBlocks, 23 | targetSubmissions: config.mineConfigs.targetSubmissions, 24 | maxShards: config.mineConfigs.maxShards, 25 | nSubtasks: config.mineConfigs.nSubtasks, 26 | subtaskInterval: config.mineConfigs.subtaskInterval, 27 | }) 28 | ).wait(); 29 | 30 | console.log(`initializing flow..`); 31 | await (await flow_.initialize(await dummyMarket_.getAddress(), config.blocksPerEpoch)).wait(); 32 | console.log(`all contract initialized.`); 33 | }; 34 | 35 | deploy.tags = ["no-market"]; 36 | deploy.dependencies = [CONTRACTS.PoraMineTest.name, CONTRACTS.Flow.name]; 37 | export default deploy; 38 | -------------------------------------------------------------------------------- /contracts/reward/ChunkLinearReward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity >=0.8.0 <0.9.0; 4 | 5 | import "./Reward.sol"; 6 | import "./ChunkRewardBase.sol"; 7 | import "../utils/ZgsSpec.sol"; 8 | import "../utils/MarketSpec.sol"; 9 | 10 | contract ChunkLinearReward is ChunkRewardBase { 11 | using RewardLibrary for Reward; 12 | 13 | uint public immutable releaseSeconds; 14 | 15 | constructor(uint releaseSeconds_) { 16 | releaseSeconds = releaseSeconds_; 17 | } 18 | 19 | function _releasedReward(Reward memory reward) internal view override returns (uint) { 20 | return reward.linearDecayReward(releaseSeconds); 21 | } 22 | 23 | function _baseReward(uint, Reward memory reward, uint) internal view override returns (uint) { 24 | if (reward.startTime != 0 && reward.startTime + releaseSeconds > block.timestamp) { 25 | return baseReward(); 26 | } else { 27 | return 0; 28 | } 29 | } 30 | 31 | function rewardDeadline(uint pricingIndex) public view returns (uint) { 32 | Reward memory reward = rewards(pricingIndex); 33 | if (reward.startTime == 0) { 34 | return 0; 35 | } 36 | return reward.startTime + releaseSeconds; 37 | } 38 | 39 | function _deadlinePassed(uint pricingIndex) internal view returns (bool) { 40 | uint deadline = rewardDeadline(pricingIndex); 41 | return deadline != 0 && deadline < block.timestamp; 42 | } 43 | 44 | function firstRewardableChunk() public view returns (uint64, uint) { 45 | uint64 low = 0; 46 | uint64 high = 1024; 47 | 48 | while (_deadlinePassed(high)) { 49 | low = high; 50 | high *= 2; 51 | } 52 | 53 | while (low < high) { 54 | uint64 mid = low + (high - low) / 2; 55 | if (_deadlinePassed(mid)) { 56 | low = mid + 1; 57 | } else { 58 | high = mid; 59 | } 60 | } 61 | 62 | return (low, block.timestamp); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/market/FixedPrice.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../interfaces/IMarket.sol"; 5 | import "../interfaces/IReward.sol"; 6 | import "../utils/MarketSpec.sol"; 7 | 8 | import "@openzeppelin/contracts/utils/Context.sol"; 9 | import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; 10 | 11 | contract FixedPrice is IMarket, AccessControlEnumerableUpgradeable { 12 | // reserved storage slots for base contract upgrade in future 13 | uint[50] private __gap; 14 | 15 | bytes32 public constant PARAMS_ADMIN_ROLE = keccak256("PARAMS_ADMIN_ROLE"); 16 | 17 | uint public pricePerSector; 18 | 19 | address public flow; 20 | address public reward; 21 | 22 | function initialize(uint pricePerSector_, address flow_, address reward_) public initializer { 23 | _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); 24 | _grantRole(PARAMS_ADMIN_ROLE, _msgSender()); 25 | 26 | pricePerSector = pricePerSector_; 27 | flow = flow_; 28 | reward = reward_; 29 | } 30 | 31 | function setPricePerSector(uint pricePerSector_) external onlyRole(PARAMS_ADMIN_ROLE) { 32 | pricePerSector = pricePerSector_; 33 | } 34 | 35 | function chargeFee(uint beforeLength, uint uploadSectors, uint paddingSectors) external { 36 | require(_msgSender() == flow, "Sender does not have permission"); 37 | 38 | uint totalSectors = uploadSectors + paddingSectors; 39 | uint baseFee = pricePerSector * uploadSectors; 40 | require(baseFee <= address(this).balance, "Not enough paid fee"); 41 | uint bonus = address(this).balance - baseFee; 42 | 43 | uint paddingPart = (baseFee * paddingSectors) / totalSectors; 44 | uint uploadPart = baseFee - paddingPart; 45 | 46 | if (paddingSectors > 0) { 47 | IReward(reward).fillReward{value: paddingPart}(beforeLength, paddingSectors); 48 | } 49 | 50 | IReward(reward).fillReward{value: bonus + uploadPart}(beforeLength + paddingSectors, uploadSectors); 51 | } 52 | 53 | receive() external payable {} 54 | } 55 | -------------------------------------------------------------------------------- /test/history.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Contract } from "ethers"; 3 | import { ethers } from "hardhat"; 4 | 5 | function testData(index: number): Uint8Array { 6 | const answer = Array(32).fill(0); 7 | answer[0] = index; 8 | answer[1] = 1; 9 | return Uint8Array.from(answer); 10 | } 11 | 12 | function toBuffer(response: string): Uint8Array { 13 | return Uint8Array.from(Buffer.from(response.substring(2), "hex")); 14 | } 15 | 16 | describe("Digest history", function () { 17 | let digestContract: Contract; 18 | beforeEach(async () => { 19 | const digestABI = await ethers.getContractFactory("DigestHistory"); 20 | digestContract = await digestABI.deploy(10); 21 | }); 22 | 23 | it("insert without override", async () => { 24 | for (let i = 0; i < 8; i++) { 25 | await digestContract.insert(testData(i)); 26 | } 27 | for (let i = 0; i < 8; i++) { 28 | expect(await digestContract.available(i)).true; 29 | expect(await digestContract.contains(testData(i))).true; 30 | expect(toBuffer(await digestContract.at(i))).to.deep.equal(testData(i)); 31 | } 32 | for (let i = 8; i < 10; i++) { 33 | expect(await digestContract.available(i)).false; 34 | await expect(digestContract.at(i)).to.be.revertedWithCustomError(digestContract, "UnavailableIndex"); 35 | } 36 | }); 37 | 38 | it("insert with override", async () => { 39 | for (let i = 0; i < 18; i++) { 40 | await digestContract.insert(testData(i)); 41 | } 42 | for (let i = 8; i < 18; i++) { 43 | expect(await digestContract.available(i)).true; 44 | expect(await digestContract.contains(testData(i))).true; 45 | expect(toBuffer(await digestContract.at(i))).to.deep.equal(testData(i)); 46 | } 47 | for (let i = 0; i < 8; i++) { 48 | expect(await digestContract.contains(testData(i))).false; 49 | expect(await digestContract.available(i)).false; 50 | await expect(digestContract.at(i)).to.be.revertedWithCustomError(digestContract, "UnavailableIndex"); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/tasks/flow.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | import { getConfig } from "../config"; 3 | import { CONTRACTS, getTypedContract } from "../utils/utils"; 4 | 5 | task("flow:show", "show contract params").setAction(async (_, hre) => { 6 | const flow = await getTypedContract(hre, CONTRACTS.FixedPriceFlow); 7 | // const signer = await hre.ethers.getSigner((await hre.getNamedAccounts()).deployer); 8 | // testnet turbo first impl 9 | // const flow = CONTRACTS.FixedPriceFlow.factory.connect("0x61450afb8F99AB3D614a45cb563C61f59d9DD026", signer); 10 | // testnet standard first impl 11 | // const flow = CONTRACTS.FixedPriceFlow.factory.connect("0x1F7A30Cd62c4132B6C521B8F79e7aE0046A4F307", signer); 12 | console.log(await flow.getContext()); 13 | console.log(await flow.blocksPerEpoch()); 14 | console.log(await flow.firstBlock()); 15 | console.log(await flow.rootHistory()); 16 | }); 17 | 18 | task("flow:setparams", "set contract params").setAction(async (_, hre) => { 19 | const flow = await getTypedContract(hre, CONTRACTS.FixedPriceFlow); 20 | const config = getConfig(hre.network.name); 21 | await (await flow.setParams(config.blocksPerEpoch, config.firstBlock, config.rootHistory)).wait(); 22 | console.log(`done.`); 23 | }); 24 | 25 | task("flow:pause", "pause contract").setAction(async (_, hre) => { 26 | const flow = await getTypedContract(hre, CONTRACTS.FixedPriceFlow); 27 | await (await flow.pause()).wait(); 28 | console.log(`done.`); 29 | }); 30 | 31 | task("flow:unpause", "unpause contract").setAction(async (_, hre) => { 32 | const flow = await getTypedContract(hre, CONTRACTS.FixedPriceFlow); 33 | await (await flow.unpause()).wait(); 34 | console.log(`done.`); 35 | }); 36 | 37 | task("flow:updatecontext", "update context to latest").setAction(async (_, hre) => { 38 | const flow = await getTypedContract(hre, CONTRACTS.FixedPriceFlow); 39 | for (;;) { 40 | const before = await flow.epoch(); 41 | await (await flow.makeContextFixedTimes(100)).wait(); 42 | const after = await flow.epoch(); 43 | if (after === before) { 44 | break; 45 | } 46 | console.log(`updated epoch to ${after}.`); 47 | } 48 | console.log(`done.`); 49 | }); 50 | -------------------------------------------------------------------------------- /match.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | def find_bytecode_in_json_files(directory, filename_pattern): 5 | bytecode_list = [] 6 | 7 | for root, dirs, files in os.walk(directory): 8 | for file in files: 9 | if filename_pattern in file and file.endswith('.json'): 10 | file_path = os.path.join(root, file) 11 | with open(file_path, 'r', encoding='utf-8') as f: 12 | try: 13 | data = json.load(f) 14 | if 'bytecode' in data: 15 | return data['bytecode'] 16 | except json.JSONDecodeError: 17 | print(f"Error decoding JSON in file: {file_path}") 18 | 19 | raise ValueError("{} not found".format(filename_pattern)) 20 | 21 | 22 | def list_json_files(directory): 23 | json_files = [] 24 | 25 | for item in os.listdir(directory): 26 | item_path = os.path.join(directory, item) 27 | if os.path.isfile(item_path) and item.endswith('Impl.json'): 28 | json_files.append(item) 29 | 30 | return json_files 31 | 32 | def check(directory): 33 | names = list_json_files(directory) 34 | matched = True 35 | for name in names: 36 | b0 = "" 37 | with open(os.path.join(directory, name), 'r', encoding='utf-8') as f: 38 | try: 39 | data = json.load(f) 40 | if 'bytecode' in data: 41 | b0 = data['bytecode'] 42 | else: 43 | raise ValueError("bytecode field not found for {}".format(name)) 44 | except json.JSONDecodeError: 45 | print(f"Error decoding JSON in file: {name}") 46 | b1 = find_bytecode_in_json_files("./artifacts", name.replace("Impl.json", ".json")) 47 | if b0 != b1: 48 | print("[ERROR] {} bytecode is different, network: {}".format(name, directory.split('/')[-1])) 49 | matched = False 50 | else: 51 | print("{} bytecode matched, network: {}".format(name, directory.split('/')[-1])) 52 | if matched: 53 | print("network {} bytecode matched".format(directory.split('/')[-1])) 54 | 55 | #check("./deployments/zgTestnetStandard") 56 | check("./deployments/zgTestnetTurbo") 57 | -------------------------------------------------------------------------------- /contracts/utils/BlockHash.sol: -------------------------------------------------------------------------------- 1 | // This file is a part of openzeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts) 2 | 3 | // SPDX-License-Identifier: MIT 4 | pragma solidity ^0.8; 5 | 6 | /** 7 | * @dev Library for accessing historical block hashes beyond the standard 256 block limit. 8 | * Uses EIP-2935's history storage contract which maintains a ring buffer of the last 9 | * 8191 block hashes in state. 10 | * 11 | * For blocks within the last 256 blocks, it uses the native `BLOCKHASH` opcode. 12 | * For blocks between 257 and 8191 blocks ago, it queries the EIP-2935 history storage. 13 | * For blocks older than 8191 or future blocks, it returns zero, matching the `BLOCKHASH` behavior. 14 | * 15 | * NOTE: After EIP-2935 activation, it takes 8191 blocks to completely fill the history. 16 | * Before that, only block hashes since the fork block will be available. 17 | */ 18 | library Blockhash { 19 | address internal constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935; 20 | 21 | /** 22 | * @dev Retrieves the block hash for any historical block within the supported range. 23 | * 24 | * NOTE: The function gracefully handles future blocks and blocks beyond the history window 25 | * by returning zero, consistent with the EVM's native `BLOCKHASH` behavior. 26 | */ 27 | function blockHash(uint256 blockNumber) internal view returns (bytes32) { 28 | uint256 current = block.number; 29 | uint256 distance; 30 | 31 | unchecked { 32 | // Can only wrap around to `current + 1` given `block.number - (2**256 - 1) = block.number + 1` 33 | distance = current - blockNumber; 34 | } 35 | 36 | return distance > 256 && distance <= 8191 ? _historyStorageCall(blockNumber) : blockhash(blockNumber); 37 | } 38 | 39 | /// @dev Internal function to query the EIP-2935 history storage contract. 40 | function _historyStorageCall(uint256 blockNumber) private view returns (bytes32 hash) { 41 | assembly ("memory-safe") { 42 | mstore(0, blockNumber) // Store the blockNumber in scratch space 43 | 44 | // In case the history storage address is not deployed, the call will succeed 45 | // without returndata, so the hash will be 0 just as querying `blockhash` directly. 46 | if and(gt(returndatasize(), 0), staticcall(gas(), HISTORY_STORAGE_ADDRESS, 0, 0x20, 0, 0x20)) { 47 | hash := mload(0) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /contracts/utils/Escrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (utils/escrow/Escrow.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "@openzeppelin/contracts/utils/Address.sol"; 8 | 9 | /** 10 | * @title Escrow 11 | * @dev Base escrow contract, holds funds designated for a payee until they 12 | * withdraw them. 13 | * 14 | * Intended usage: This contract (and derived escrow contracts) should be a 15 | * standalone contract, that only interacts with the contract that instantiated 16 | * it. That way, it is guaranteed that all Ether will be handled according to 17 | * the `Escrow` rules, and there is no need to check for payable functions or 18 | * transfers in the inheritance tree. The contract that uses the escrow as its 19 | * payment method should be its owner, and provide public methods redirecting 20 | * to the escrow's deposit and withdraw. 21 | */ 22 | contract Escrow is Ownable { 23 | using Address for address payable; 24 | 25 | event Deposited(address indexed payee, uint256 weiAmount); 26 | event Withdrawn(address indexed payee, uint256 weiAmount); 27 | 28 | mapping(address => uint256) private _deposits; 29 | 30 | constructor() Ownable(msg.sender) {} 31 | 32 | function depositsOf(address payee) public view returns (uint256) { 33 | return _deposits[payee]; 34 | } 35 | 36 | /** 37 | * @dev Stores the sent amount as credit to be withdrawn. 38 | * @param payee The destination address of the funds. 39 | * 40 | * Emits a {Deposited} event. 41 | */ 42 | function deposit(address payee) public payable virtual onlyOwner { 43 | uint256 amount = msg.value; 44 | _deposits[payee] += amount; 45 | emit Deposited(payee, amount); 46 | } 47 | 48 | /** 49 | * @dev Withdraw accumulated balance for a payee, forwarding all gas to the 50 | * recipient. 51 | * 52 | * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. 53 | * Make sure you trust the recipient, or are either following the 54 | * checks-effects-interactions pattern or using {ReentrancyGuard}. 55 | * 56 | * @param payee The address whose funds will be withdrawn and transferred to. 57 | * 58 | * Emits a {Withdrawn} event. 59 | */ 60 | function withdraw(address payable payee) public virtual onlyOwner { 61 | uint256 payment = _deposits[payee]; 62 | 63 | _deposits[payee] = 0; 64 | 65 | payee.sendValue(payment); 66 | 67 | emit Withdrawn(payee, payment); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Flow Context Update Daemon 2 | 3 | Single script to automatically update flow context daily at 2:00 AM using nohup. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Set your private key for gas payments 9 | export DEPLOYER_KEY="your_private_key_without_0x" 10 | 11 | # Start daemon (runs in background) 12 | ./scripts/flow-update.sh start 13 | 14 | # Check status 15 | ./scripts/flow-update.sh status 16 | ``` 17 | 18 | ## Commands 19 | 20 | ```bash 21 | ./scripts/flow-update.sh start # Start daemon with nohup 22 | ./scripts/flow-update.sh stop # Stop daemon 23 | ./scripts/flow-update.sh status # Show status and PID 24 | ./scripts/flow-update.sh logs # View logs (real-time) 25 | ./scripts/flow-update.sh run # Run update once manually 26 | ./scripts/flow-update.sh restart # Restart daemon 27 | ``` 28 | 29 | ## Configuration 30 | 31 | ```bash 32 | # Required for gas payments 33 | export DEPLOYER_KEY="your_private_key_without_0x" 34 | 35 | # Optional network (default: zgTestnetTurbo) 36 | export FLOW_UPDATE_NETWORK="mainnet" 37 | ``` 38 | 39 | ## How It Works 40 | 41 | - Daemon runs in background using `nohup` 42 | - Executes `hardhat flow:updatecontext` daily at 2:00 AM 43 | - Prevents multiple runs per day 44 | - Comprehensive logging with timestamps 45 | - Automatic restart capability 46 | 47 | ## Logs 48 | 49 | - Daily logs: `logs/flow-update-YYYYMMDD.log` 50 | - Daemon logs: `logs/daemon-YYYYMMDD-HHMMSS.log` 51 | - PID file: `logs/flow-daemon.pid` 52 | 53 | ## Troubleshooting 54 | 55 | ```bash 56 | # Check if running 57 | ./scripts/flow-update.sh status 58 | 59 | # View real-time logs 60 | ./scripts/flow-update.sh logs 61 | 62 | # Test daemon without starting 63 | ./scripts/flow-update.sh debug 64 | 65 | # Restart if stuck 66 | ./scripts/flow-update.sh restart 67 | ``` 68 | 69 | Single script to automatically update flow context daily at 2:00 AM. 70 | 71 | ## Quick Start 72 | 73 | ```bash 74 | # Set your private key for gas payments 75 | export DEPLOYER_KEY="your_private_key_without_0x" 76 | export FLOW_UPDATE_NETWORK="zgTestnetTurbo" # Optional network 77 | 78 | # Start daemon 79 | ./scripts/flow-update.sh start 80 | 81 | # Check status 82 | ./scripts/flow-update.sh status 83 | ``` 84 | 85 | ## Commands 86 | 87 | ```bash 88 | ./scripts/flow-update.sh start # Start daemon in tmux 89 | ./scripts/flow-update.sh stop # Stop daemon 90 | ./scripts/flow-update.sh status # Show status 91 | ./scripts/flow-update.sh logs # View logs 92 | ./scripts/flow-update.sh attach # Attach to tmux session 93 | ./scripts/flow-update.sh run # Run update once manually 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /contracts/interfaces/IFlow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import "./Submission.sol"; 4 | import "./IDigestHistory.sol"; 5 | 6 | struct EpochRange { 7 | uint128 start; 8 | uint128 end; 9 | } 10 | 11 | struct EpochRangeWithContextDigest { 12 | uint128 start; 13 | uint128 end; 14 | bytes32 digest; 15 | } 16 | 17 | struct MineContext { 18 | uint epoch; 19 | uint mineStart; 20 | bytes32 flowRoot; 21 | uint flowLength; 22 | bytes32 blockDigest; 23 | bytes32 digest; 24 | } 25 | 26 | interface IFlow { 27 | event Submit( 28 | address indexed sender, 29 | bytes32 indexed identity, 30 | uint submissionIndex, 31 | uint startPos, 32 | uint length, 33 | Submission submission 34 | ); 35 | 36 | event NewEpoch( 37 | address indexed sender, 38 | uint indexed index, 39 | bytes32 startMerkleRoot, 40 | uint submissionIndex, 41 | uint flowLength, 42 | bytes32 context 43 | ); 44 | 45 | function batchSubmit( 46 | Submission[] memory submissions 47 | ) 48 | external 49 | payable 50 | returns (uint[] memory indexes, bytes32[] memory digests, uint[] memory startIndexes, uint[] memory lengths); 51 | 52 | function blocksPerEpoch() external view returns (uint); 53 | 54 | function epoch() external view returns (uint); 55 | 56 | function epochStartPosition() external view returns (uint); 57 | 58 | function firstBlock() external view returns (uint); 59 | 60 | function getContext() external view returns (MineContext memory); 61 | 62 | function getEpochRange(bytes32 digest) external view returns (EpochRange memory); 63 | 64 | function getFlowRootByTxSeq(uint txSeq) external view returns (bytes32); 65 | 66 | function makeContext() external; 67 | 68 | function makeContextFixedTimes(uint cnt) external; 69 | 70 | function makeContextWithResult() external returns (MineContext memory); 71 | 72 | function market() external view returns (address payable); 73 | 74 | function numSubmissions() external view returns (uint); 75 | 76 | function queryContextAtPosition(uint128 targetPosition) external returns (EpochRangeWithContextDigest memory range); 77 | 78 | function rootHistory() external view returns (IDigestHistory); 79 | 80 | function submissionIndex() external view returns (uint); 81 | 82 | function submit(Submission memory submission) external payable returns (uint, bytes32, uint, uint); 83 | 84 | function tree() external view returns (uint currentLength, uint unstagedHeight); 85 | } 86 | -------------------------------------------------------------------------------- /contracts/interfaces/Submission.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | struct SubmissionNode { 5 | bytes32 root; 6 | uint height; 7 | } 8 | 9 | struct Submission { 10 | uint length; 11 | bytes tags; 12 | SubmissionNode[] nodes; 13 | address submitter; 14 | } 15 | 16 | library SubmissionLibrary { 17 | uint public constant MAX_DEPTH = 64; 18 | uint public constant ENTRY_SIZE = 256; 19 | uint public constant MAX_LENGTH = 4; 20 | 21 | function size(Submission memory submission) internal pure returns (uint) { 22 | uint _size = 0; 23 | for (uint i = 0; i < submission.nodes.length; i++) { 24 | _size += 1 << submission.nodes[i].height; 25 | } 26 | return _size; 27 | } 28 | 29 | function valid(Submission memory submission) internal pure returns (bool) { 30 | if (submission.nodes.length == 0) { 31 | return false; 32 | } 33 | 34 | // Solidity 0.8 has overflow checking by default. 35 | if (submission.nodes[0].height - submission.nodes[submission.nodes.length - 1].height >= MAX_LENGTH) { 36 | return false; 37 | } 38 | 39 | if (submission.nodes[0].height >= MAX_DEPTH) { 40 | return false; 41 | } 42 | 43 | for (uint i = 0; i < submission.nodes.length - 1; i++) { 44 | if (submission.nodes[i + 1].height >= submission.nodes[i].height) { 45 | return false; 46 | } 47 | } 48 | 49 | uint submissionCapacity = size(submission); 50 | 51 | if (submission.length > submissionCapacity * ENTRY_SIZE) { 52 | return false; 53 | } 54 | 55 | uint lastCapacity; 56 | if (submissionCapacity < (1 << MAX_LENGTH)) { 57 | lastCapacity = submissionCapacity - 1; 58 | } else if (submission.nodes.length == 1) { 59 | lastCapacity = submissionCapacity - (submissionCapacity >> MAX_LENGTH); 60 | } else { 61 | lastCapacity = submissionCapacity - (1 << (submission.nodes[0].height - MAX_LENGTH + 1)); 62 | } 63 | 64 | if (submission.length <= lastCapacity * ENTRY_SIZE) { 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | 71 | function digest(Submission memory submission) internal pure returns (bytes32) { 72 | bytes32[] memory hashes = new bytes32[](submission.nodes.length); 73 | 74 | for (uint i = 0; i < submission.nodes.length; i++) { 75 | hashes[i] = submission.nodes[i].root; 76 | } 77 | 78 | return keccak256(abi.encodePacked(hashes)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/miner/RecallRange.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "@openzeppelin/contracts/utils/math/Math.sol"; 5 | import "../utils/ZgsSpec.sol"; 6 | 7 | struct RecallRange { 8 | uint startPosition; 9 | uint mineLength; 10 | uint64 shardId; 11 | uint64 shardMask; 12 | } 13 | 14 | library RecallRangeLib { 15 | using RecallRangeLib for RecallRange; 16 | 17 | function check(RecallRange memory range, uint maxEndPosition) internal pure { 18 | require(range.startPosition % SECTORS_PER_PRICE == 0, "Start position is not aligned"); 19 | 20 | require(range.startPosition + range.mineLength <= maxEndPosition, "Mining range overflow"); 21 | 22 | uint maxMineLength = MAX_MINING_LENGTH * range.numShards(); 23 | require(range.mineLength <= maxMineLength, "Mining range too long"); 24 | 25 | uint requiredLength = Math.min(maxEndPosition, maxMineLength); 26 | require(range.mineLength >= requiredLength, "Mining range too short"); 27 | 28 | require(range.shardId & range.shardMask == 0, "Masked bits should be zero"); 29 | } 30 | 31 | // Compute bit `0` in shardMask, while assuming there is only a few zeros. 32 | function numShards(RecallRange memory range) internal pure returns (uint) { 33 | uint64 negMask = ~range.shardMask; 34 | uint8 answer = 0; 35 | while (negMask > 0) { 36 | negMask &= negMask - 1; 37 | answer++; 38 | } 39 | return 1 << answer; 40 | } 41 | 42 | function targetScaleX64(RecallRange memory range, uint flowLength) internal pure returns (uint) { 43 | uint noShardMineLength = flowLength > MAX_MINING_LENGTH ? MAX_MINING_LENGTH : flowLength; 44 | uint shardLength = flowLength / numShards(range); 45 | uint actualMineLength = shardLength > MAX_MINING_LENGTH ? MAX_MINING_LENGTH : shardLength; 46 | return (noShardMineLength << 64) / actualMineLength; 47 | } 48 | 49 | function digest(RecallRange memory range) internal pure returns (bytes32) { 50 | return keccak256(abi.encode(range.startPosition, range.mineLength, range.shardId, range.shardMask)); 51 | } 52 | 53 | function recallChunk(RecallRange memory range, bytes32 seed) internal pure returns (uint) { 54 | uint originChunkOffset = uint(seed) % (range.mineLength / SECTORS_PER_LOAD); 55 | uint64 chunkOffset = (uint64(originChunkOffset) & range.shardMask) | range.shardId; 56 | 57 | require(chunkOffset * SECTORS_PER_LOAD <= range.mineLength, "Recall position out of bound"); 58 | 59 | return range.startPosition + chunkOffset * SECTORS_PER_LOAD; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/deploy/deploy_market_enabled_initialization.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { getConfig } from "../config"; 4 | import { CONTRACTS, getTypedContract } from "../utils/utils"; 5 | 6 | const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | // initialize all contracts 8 | const config = getConfig(hre.network.name); 9 | const poraMine_ = await getTypedContract(hre, CONTRACTS.PoraMine); 10 | const fixedPriceMarket_ = await getTypedContract(hre, CONTRACTS.FixedPrice); 11 | const chunkLinearReward_ = await getTypedContract(hre, CONTRACTS.ChunkLinearReward); 12 | const fixedPriceFlow_ = await getTypedContract(hre, CONTRACTS.FixedPriceFlow); 13 | 14 | const flowAddress = await fixedPriceFlow_.getAddress(); 15 | const rewardAddress = await chunkLinearReward_.getAddress(); 16 | const marketAddress = await fixedPriceMarket_.getAddress(); 17 | const mineAddress = await poraMine_.getAddress(); 18 | 19 | console.log(`initializing pora mine..`); 20 | await ( 21 | await poraMine_.initialize(flowAddress, rewardAddress, { 22 | difficulty: config.mineConfigs.initDifficulty, 23 | targetMineBlocks: config.mineConfigs.targetMineBlocks, 24 | targetSubmissions: config.mineConfigs.targetSubmissions, 25 | maxShards: config.mineConfigs.maxShards, 26 | nSubtasks: config.mineConfigs.nSubtasks, 27 | subtaskInterval: config.mineConfigs.subtaskInterval, 28 | }) 29 | ).wait(); 30 | 31 | console.log(`initializing fixed price market..`); 32 | // Use `lifetimeMonth * MONTH_ZGAS_UNITS_PER_SECTOR` as `pricePerSector` 33 | await ( 34 | await fixedPriceMarket_.initialize( 35 | (BigInt(config.lifetimeMonth * config.unitPrice) * 1_000_000_000_000_000_000n) / 36 | 1024n / 37 | 1024n / 38 | 1024n / 39 | 12n, 40 | flowAddress, 41 | rewardAddress 42 | ) 43 | ).wait(); 44 | 45 | console.log(`initializing chunk linear reward..`); 46 | await ( 47 | await chunkLinearReward_.initialize(marketAddress, mineAddress, config.chunkRewardConfigs.foundationAdmin) 48 | ).wait(); 49 | 50 | console.log(`initializing fixed price flow..`); 51 | await (await fixedPriceFlow_.initialize(marketAddress, config.blocksPerEpoch)).wait(); 52 | console.log(`all contract initialized.`); 53 | }; 54 | 55 | deploy.tags = ["market-enabled"]; 56 | deploy.dependencies = [ 57 | CONTRACTS.PoraMine.name, 58 | CONTRACTS.ChunkLinearReward.name, 59 | CONTRACTS.FixedPrice.name, 60 | CONTRACTS.FixedPriceFlow.name, 61 | ]; 62 | export default deploy; 63 | -------------------------------------------------------------------------------- /test/flow.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import hre, { deployments } from "hardhat"; 3 | import { CONTRACTS, getTypedContract } from "../src/utils/utils"; 4 | import { Flow } from "../typechain-types"; 5 | 6 | describe("ZeroGStorage Flow", function () { 7 | let flow: Flow; 8 | 9 | before(async () => { 10 | await deployments.fixture("no-market"); 11 | 12 | flow = await getTypedContract(hre, CONTRACTS.Flow); 13 | }); 14 | 15 | it("submit 256 sectors, in segment #0", async () => { 16 | const root = Buffer.from("ccc2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", "hex"); 17 | const result = await flow.submit.staticCall({ 18 | length: 256 * 256, 19 | tags: Buffer.from(""), 20 | nodes: [{ root, height: 8 }], 21 | }); 22 | expect(result[0]).to.deep.eq(0n); 23 | expect(result[2]).to.deep.eq(256n); 24 | expect(result[3]).to.deep.eq(256n); 25 | await flow.submit({ length: 256 * 256, tags: Buffer.from(""), nodes: [{ root, height: 8 }] }); 26 | }); 27 | 28 | it("submit 960 sectors, pad to segment #1", async () => { 29 | const root = Buffer.from("ccc2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", "hex"); 30 | const result = await flow.submit.staticCall({ 31 | length: 960 * 256, 32 | tags: Buffer.from(""), 33 | nodes: [ 34 | { root, height: 9 }, 35 | { root, height: 8 }, 36 | { root, height: 7 }, 37 | { root, height: 6 }, 38 | ], 39 | }); 40 | expect(result[0]).to.deep.eq(1n); 41 | expect(result[2]).to.deep.eq(1024n); 42 | expect(result[3]).to.deep.eq(960n); 43 | await flow.submit({ 44 | length: 960 * 256, 45 | tags: Buffer.from(""), 46 | nodes: [ 47 | { root, height: 9 }, 48 | { root, height: 8 }, 49 | { root, height: 7 }, 50 | { root, height: 6 }, 51 | ], 52 | }); 53 | }); 54 | 55 | it("submit 960 sectors, pad to segment #2", async () => { 56 | const root = Buffer.from("ccc2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", "hex"); 57 | const result = await flow.submit.staticCall({ 58 | length: 960 * 256, 59 | tags: Buffer.from(""), 60 | nodes: [ 61 | { root, height: 9 }, 62 | { root, height: 8 }, 63 | { root, height: 7 }, 64 | { root, height: 6 }, 65 | ], 66 | }); 67 | expect(result[0]).to.deep.eq(2n); 68 | expect(result[2]).to.deep.eq(2048n); 69 | expect(result[3]).to.deep.eq(960n); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "0g-storage-contracts", 3 | "author": "Zero Gravity Labs", 4 | "version": "0.0.1", 5 | "license": "Unlicense", 6 | "scripts": { 7 | "build": "hardhat compile", 8 | "test": "hardhat test", 9 | "coverage": "hardhat coverage", 10 | "deploy-market-enabled": "hardhat deploy --tags market-enabled --network", 11 | "deploy-no-market": "hardhat deploy --tags no-market --network", 12 | "lint": "yarn lint:sol && yarn lint:ts", 13 | "lint:sol": "solhint 'contracts/**/*.sol'", 14 | "lint:ts": "eslint 'src/**/*.ts' '*.ts' --max-warnings 0 --fix", 15 | "fmt:sol": "prettier 'contracts/**/*.sol' -w", 16 | "fmt:ts": "prettier 'src/**/*.ts' 'test/**/*.ts' '*.ts' -w", 17 | "postinstall": "patch-package" 18 | }, 19 | "devDependencies": { 20 | "@clrfund/waffle-mock-contract": "^0.0.11", 21 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", 22 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 23 | "@nomicfoundation/hardhat-ignition": "^0.15.5", 24 | "@nomicfoundation/hardhat-ignition-ethers": "^0.15.5", 25 | "@nomicfoundation/hardhat-network-helpers": "^1.0.11", 26 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 27 | "@nomicfoundation/ignition-core": "^0.15.5", 28 | "@openzeppelin/contracts": "5.0.2", 29 | "@openzeppelin/contracts-upgradeable": "5.0.2", 30 | "@openzeppelin/hardhat-upgrades": "^3.5.0", 31 | "@typechain/ethers-v6": "^0.5.1", 32 | "@typechain/hardhat": "^9.1.0", 33 | "@types/buffer-xor": "^2.0.2", 34 | "@types/mocha": "^10.0.6", 35 | "@typescript-eslint/eslint-plugin": "6.21.0", 36 | "@typescript-eslint/parser": "6.21.0", 37 | "chai": "4.3.7", 38 | "dotenv": "^16.4.5", 39 | "eslint": "8.34.0", 40 | "eslint-config-prettier": "8.6.0", 41 | "eslint-plugin-no-only-tests": "3.1.0", 42 | "eslint-plugin-prettier": "4.2.1", 43 | "ethers": "^6.11.1", 44 | "hardhat": "^2.22.2", 45 | "hardhat-abi-exporter": "^2.10.1", 46 | "hardhat-deploy": "^0.14", 47 | "hardhat-deploy-ethers": "^0.4.1", 48 | "hardhat-gas-reporter": "^2.1.1", 49 | "hardhat-interface-generator": "^0.0.6", 50 | "hardhat-tracer": "^3.1.0", 51 | "hash-wasm": "^4.9.0", 52 | "patch-package": "^8.0.0", 53 | "prettier": "2.8.4", 54 | "prettier-plugin-organize-imports": "3.2.4", 55 | "prettier-plugin-solidity": "1.1.2", 56 | "solhint": "^4.5.4", 57 | "solhint-plugin-prettier": "0.0.5", 58 | "solidity-coverage": "^0.8.12", 59 | "ts-node": "^10.9.2", 60 | "typechain": "^8.3.2", 61 | "typescript": "4.9.5" 62 | }, 63 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", 64 | "dependencies": { 65 | "@nomicfoundation/hardhat-verify": "^2.0.8" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/reward/Reward.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../utils/Exponent.sol"; 5 | import "../utils/ZgsSpec.sol"; 6 | 7 | struct Reward { 8 | uint128 lockedReward; 9 | uint128 claimableReward; 10 | uint128 distributedReward; 11 | uint40 startTime; 12 | uint40 lastUpdate; 13 | } 14 | 15 | library RewardLibrary { 16 | function addReward(Reward storage reward, uint amount, bool finalized) internal { 17 | require(amount <= type(uint128).max, "Reward overflow"); 18 | require(reward.startTime == 0, "Reward item has been initialized"); 19 | 20 | reward.lockedReward += uint128(amount); 21 | if (finalized) { 22 | reward.startTime = uint40(block.timestamp); 23 | reward.lastUpdate = uint40(block.timestamp); 24 | } 25 | } 26 | 27 | function updateReward(Reward memory reward, uint releaseReward) internal view { 28 | reward.lockedReward -= uint128(releaseReward); 29 | reward.claimableReward += uint128(releaseReward); 30 | reward.lastUpdate = uint40(block.timestamp); 31 | } 32 | 33 | function expDecayReward(Reward memory reward, uint annualMilliDecayRate) internal view returns (uint) { 34 | if (reward.startTime == 0) { 35 | return 0; 36 | } 37 | 38 | uint timeElapsed = (block.timestamp - reward.lastUpdate) * 1000; 39 | uint decayX64 = (timeElapsed * uint(Exponential.INV_LOG2X128) * annualMilliDecayRate) / 40 | (1 << 64) / 41 | MILLI_SECONDS_PER_YEAR / 42 | 1000; 43 | uint releaseX96 = (1 << 96) - Exponential.powHalf64X96(decayX64); 44 | return (releaseX96 * uint(reward.lockedReward)) / (1 << 96); 45 | } 46 | 47 | function linearDecayReward(Reward memory reward, uint releaseSeconds) internal view returns (uint) { 48 | if (reward.startTime == 0) { 49 | return 0; 50 | } 51 | 52 | uint releasedReward = reward.claimableReward + reward.distributedReward; 53 | uint totalReward = reward.lockedReward + releasedReward; 54 | 55 | uint timeElapsedSinceLaunch = block.timestamp - reward.startTime; 56 | 57 | uint expectedReleasedReward = (totalReward * timeElapsedSinceLaunch) / releaseSeconds; 58 | if (expectedReleasedReward > totalReward) { 59 | expectedReleasedReward = totalReward; 60 | } 61 | if (expectedReleasedReward < releasedReward) { 62 | return 0; 63 | } 64 | return expectedReleasedReward - releasedReward; 65 | } 66 | 67 | function claimReward(Reward memory reward) internal pure returns (uint amount) { 68 | if (reward.startTime == 0) { 69 | return 0; 70 | } 71 | uint128 claimedReward = reward.claimableReward / 2; 72 | reward.claimableReward -= claimedReward; 73 | reward.distributedReward += claimedReward; 74 | 75 | return uint(claimedReward); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ZeroAddress } from "ethers"; 2 | import { ZerogContractConfigs } from "./networks/zerog_contract_config"; 3 | import { ZerogTestnetContractConfigsStandard } from "./networks/zerog_testnet_contract_config_standard"; 4 | import { ZerogTestnetContractConfigsTurbo } from "./networks/zerog_testnet_contract_config_turbo"; 5 | import { ZerogMainnetContractConfigsTurbo } from "./networks/zerog_mainnet_contract_config_turbo"; 6 | import { ZerogMainnetContractConfigsStandard } from "./networks/zerog_mainnet_contract_config_standard"; 7 | 8 | export interface MineConfigs { 9 | settings: number; 10 | // The initial difficulty for PoRA mining. 11 | initDifficulty: number; 12 | // Mining window duration for each subtask in blocks 13 | targetMineBlocks: number; 14 | // Target number of submissions per epoch 15 | targetSubmissions: number; 16 | // Maximum shards per mining submission 17 | maxShards: number; 18 | // Number of subtasks per epoch 19 | nSubtasks: number; 20 | // Interval between subtask start times in blocks 21 | subtaskInterval: number; 22 | } 23 | 24 | export interface ChunkRewardConfigs { 25 | // Foundation admin address for chunk reward management 26 | foundationAdmin: string; 27 | } 28 | 29 | export interface NetworkConfigs { 30 | mineConfigs: MineConfigs; 31 | chunkRewardConfigs: ChunkRewardConfigs; 32 | // This variable determines how often the `makeContext` function needs to be called within each mining cycle, known as an Epoch. If this function is not called over several epochs, it may cause issues. By default, this value is set very high, meaning that the contract will not generate mining tasks. For mining tests, adjust it to a suitable size (recommended block count per hour). 33 | blocksPerEpoch: number; 34 | firstBlock: number; 35 | rootHistory: string; 36 | // Upon enabling the economic model, this controls the data storage validity period and the reward release cycle. The annual storage cost per GB is a constant in the contract named `ANNUAL_ZGS_TOKENS_PER_GB`. 37 | lifetimeMonth: number; 38 | flowDeployDelay: number; 39 | unitPrice: number; 40 | } 41 | 42 | export const DefaultConfig: NetworkConfigs = { 43 | mineConfigs: { 44 | settings: 0, 45 | initDifficulty: 30000, 46 | targetMineBlocks: 100, 47 | targetSubmissions: 10, 48 | maxShards: 32, 49 | nSubtasks: 1, 50 | subtaskInterval: 100, 51 | }, 52 | chunkRewardConfigs: { 53 | foundationAdmin: ZeroAddress, 54 | }, 55 | blocksPerEpoch: 1000000000, 56 | firstBlock: 0, 57 | rootHistory: ZeroAddress, 58 | lifetimeMonth: 3, 59 | flowDeployDelay: 0, 60 | unitPrice: 1, 61 | }; 62 | 63 | export const GlobalConfig: { [key: string]: NetworkConfigs } = { 64 | zg: ZerogContractConfigs, 65 | zgTestnetStandard: ZerogTestnetContractConfigsStandard, 66 | zgTestnetTurbo: ZerogTestnetContractConfigsTurbo, 67 | zgTurbo: ZerogMainnetContractConfigsTurbo, 68 | zgStandard: ZerogMainnetContractConfigsStandard, 69 | }; 70 | 71 | export function getConfig(network: string) { 72 | if (network in GlobalConfig) return GlobalConfig[network]; 73 | if (network === "hardhat") { 74 | return DefaultConfig; 75 | } 76 | throw new Error(`network ${network} non-exist`); 77 | } 78 | -------------------------------------------------------------------------------- /test/blake2b.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { deployments, ethers } from "hardhat"; 3 | import { blake2b } from "hash-wasm"; 4 | import { Blake2bTest } from "../typechain-types"; 5 | 6 | function bufferResponse(response: string[]) { 7 | return Uint8Array.from(Buffer.concat(response.map((x: string) => Buffer.from(x.substring(2), "hex")))); 8 | } 9 | 10 | function bufferAnswer(answer: string) { 11 | return Uint8Array.from(Buffer.from(answer, "hex")); 12 | } 13 | 14 | describe("Blake2b hash", function () { 15 | let blake2bContract: Blake2bTest; 16 | before(async () => { 17 | await deployments.fixture("blake2b-test"); 18 | const blakeABI = await ethers.getContractFactory("Blake2bTest"); 19 | blake2bContract = await blakeABI.deploy(); 20 | }); 21 | 22 | it("hash empty", async () => { 23 | const input = Uint8Array.from([]); 24 | const answer = bufferAnswer(await blake2b(input)); 25 | const response = bufferResponse(await blake2bContract.blake2b([])); 26 | expect(response).to.deep.equal(answer); 27 | }); 28 | 29 | it("hash one entry", async () => { 30 | const input = Buffer.from("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "hex"); 31 | const answer = bufferAnswer(await blake2b(input)); 32 | const response = bufferResponse(await blake2bContract.blake2b([input])); 33 | expect(response).to.deep.equal(answer); 34 | }); 35 | 36 | it("hash two entries", async () => { 37 | const input0 = Buffer.from("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "hex"); 38 | const input1 = Buffer.from("101112131415161718191a1b1c1d1e1f101112131415161718191a1b1c1d1e1f", "hex"); 39 | const answer = bufferAnswer(await blake2b(Buffer.concat([input0, input1]))); 40 | const response = bufferResponse(await blake2bContract.blake2b([input0, input1])); 41 | expect(response).to.deep.equal(answer); 42 | 43 | const response2 = bufferResponse(await blake2bContract.blake2bPair([input0, input1])); 44 | expect(response2).to.deep.equal(answer); 45 | }); 46 | 47 | it("hash three entries", async () => { 48 | const input0 = Buffer.from("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "hex"); 49 | const input1 = Buffer.from("101112131415161718191a1b1c1d1e1f101112131415161718191a1b1c1d1e1f", "hex"); 50 | const input2 = Buffer.from("202122232425262728292a2b2c2d2e2f202122232425262728292a2b2c2d2e2f", "hex"); 51 | const answer = bufferAnswer(await blake2b(Buffer.concat([input0, input1, input2]))); 52 | const response = bufferResponse(await blake2bContract.blake2b([input0, input1, input2])); 53 | expect(response).to.deep.equal(answer); 54 | 55 | const response2 = bufferResponse(await blake2bContract.blake2bTriple([input0, input1, input2])); 56 | expect(response2).to.deep.equal(answer); 57 | }); 58 | 59 | it("hash five entries", async () => { 60 | const input0 = Buffer.from("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "hex"); 61 | const input1 = Buffer.from("101112131415161718191a1b1c1d1e1f101112131415161718191a1b1c1d1e1f", "hex"); 62 | const input2 = Buffer.from("202122232425262728292a2b2c2d2e2f202122232425262728292a2b2c2d2e2f", "hex"); 63 | const answer = bufferAnswer(await blake2b(Buffer.concat([input0, input1, input2, input0, input1]))); 64 | const response = bufferResponse(await blake2bContract.blake2b([input0, input1, input2, input0, input1])); 65 | expect(response).to.deep.equal(answer); 66 | 67 | const response2 = bufferResponse(await blake2bContract.blake2bFive([input0, input1, input2, input0, input1])); 68 | expect(response2).to.deep.equal(answer); 69 | }); 70 | 71 | it("hash large chunk", async () => { 72 | const input = Array(20).fill( 73 | Buffer.from("000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", "hex") 74 | ); 75 | 76 | const answer = bufferAnswer(await blake2b(Buffer.concat(input))); 77 | const response = bufferResponse(await blake2bContract.blake2b(input)); 78 | expect(response).to.deep.equal(answer); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /contracts/utils/PullPayment.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Modified from OpenZeppelin Contracts (last updated v4.8.0) (security/PullPayment.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "./Escrow.sol"; 7 | 8 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 9 | 10 | /** 11 | * @dev Simple implementation of a 12 | * https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/#favor-pull-over-push-for-external-calls[pull-payment] 13 | * strategy, where the paying contract doesn't interact directly with the 14 | * receiver account, which must withdraw its payments itself. 15 | * 16 | * Pull-payments are often considered the best practice when it comes to sending 17 | * Ether, security-wise. It prevents recipients from blocking execution, and 18 | * eliminates reentrancy concerns. 19 | * 20 | * TIP: If you would like to learn more about reentrancy and alternative ways 21 | * to protect against it, check out our blog post 22 | * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. 23 | * 24 | * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} 25 | * instead of Solidity's `transfer` function. Payees can query their due 26 | * payments with {payments}, and retrieve them with {withdrawPayments}. 27 | */ 28 | abstract contract PullPayment is Initializable { 29 | /// @custom:storage-location erc7201:0g.storage.PullPayment 30 | struct PullPaymentStorage { 31 | Escrow escrow; 32 | } 33 | 34 | // keccak256(abi.encode(uint(keccak256("0g.storage.PullPayment")) - 1)) & ~bytes32(uint(0xff)) 35 | bytes32 private constant PullPaymentStorageLocation = 36 | 0x18886ccf3cb33ec4f8e31fd4f09d61266d4695ceab87fb3d39636905b707c100; 37 | 38 | function _getPullPaymentStorage() private pure returns (PullPaymentStorage storage $) { 39 | assembly { 40 | $.slot := PullPaymentStorageLocation 41 | } 42 | } 43 | 44 | function __PullPayment_init() internal onlyInitializing { 45 | PullPaymentStorage storage $ = _getPullPaymentStorage(); 46 | $.escrow = new Escrow(); 47 | } 48 | 49 | function _escrow() internal view returns (Escrow) { 50 | PullPaymentStorage storage $ = _getPullPaymentStorage(); 51 | return $.escrow; 52 | } 53 | 54 | /** 55 | * @dev Withdraw accumulated payments, forwarding all gas to the recipient. 56 | * 57 | * Note that _any_ account can call this function, not just the `payee`. 58 | * This means that contracts unaware of the `PullPayment` protocol can still 59 | * receive funds this way, by having a separate account call 60 | * {withdrawPayments}. 61 | * 62 | * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. 63 | * Make sure you trust the recipient, or are either following the 64 | * checks-effects-interactions pattern or using {ReentrancyGuard}. 65 | * 66 | * @param payee Whose payments will be withdrawn. 67 | * 68 | * Causes the `escrow` to emit a {Withdrawn} event. 69 | */ 70 | function withdrawPayments(address payable payee) public virtual { 71 | PullPaymentStorage storage $ = _getPullPaymentStorage(); 72 | $.escrow.withdraw(payee); 73 | } 74 | 75 | /** 76 | * @dev Returns the payments owed to an address. 77 | * @param dest The creditor's address. 78 | */ 79 | function payments(address dest) public view returns (uint) { 80 | PullPaymentStorage storage $ = _getPullPaymentStorage(); 81 | return $.escrow.depositsOf(dest); 82 | } 83 | 84 | /** 85 | * @dev Called by the payer to store the sent amount as credit to be pulled. 86 | * Funds sent in this way are stored in an intermediate {Escrow} contract, so 87 | * there is no danger of them being spent before withdrawal. 88 | * 89 | * @param dest The destination address of the funds. 90 | * @param amount The amount to transfer. 91 | * 92 | * Causes the `escrow` to emit a {Deposited} event. 93 | */ 94 | function _asyncTransfer(address dest, uint amount) internal virtual { 95 | PullPaymentStorage storage $ = _getPullPaymentStorage(); 96 | $.escrow.deposit{value: amount}(dest); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@nomicfoundation/hardhat-toolbox"; 2 | import "@nomicfoundation/hardhat-verify"; 3 | import "@openzeppelin/hardhat-upgrades"; 4 | import "hardhat-abi-exporter"; 5 | import "hardhat-deploy"; 6 | import "hardhat-deploy-ethers"; 7 | import "hardhat-gas-reporter"; 8 | import "hardhat-interface-generator"; 9 | import { HardhatUserConfig, HttpNetworkUserConfig } from "hardhat/types"; 10 | import "solidity-coverage"; 11 | 12 | // environment configs 13 | import dotenv from "dotenv"; 14 | dotenv.config(); 15 | const { NODE_URL, DEPLOYER_KEY, ETHERSCAN_API_KEY } = process.env; 16 | 17 | // 0x12cAef034a8D1548a81fd7d677640d1070a1Ec17 18 | const DEFAULT_DEPLOYER = "36b9e861b63d3509c88b7817275a30d22d62c8cd8fa6486ddee35ef0d8e0495f"; 19 | 20 | const userConfig: HttpNetworkUserConfig = { 21 | accounts: [DEPLOYER_KEY ? DEPLOYER_KEY : DEFAULT_DEPLOYER], 22 | }; 23 | 24 | import "./src/tasks/access"; 25 | import "./src/tasks/codesize"; 26 | import "./src/tasks/flow"; 27 | import "./src/tasks/mine"; 28 | import "./src/tasks/reward"; 29 | import "./src/tasks/upgrade"; 30 | 31 | const config: HardhatUserConfig = { 32 | paths: { 33 | artifacts: "artifacts", 34 | cache: "build/cache", 35 | sources: "contracts", 36 | deploy: "src/deploy", 37 | }, 38 | solidity: { 39 | compilers: [ 40 | { 41 | version: "0.8.20", 42 | settings: { 43 | outputSelection: { 44 | "*": { 45 | "*": [ 46 | "evm.bytecode.object", 47 | "evm.deployedBytecode.object", 48 | "abi", 49 | "evm.bytecode.sourceMap", 50 | "evm.deployedBytecode.sourceMap", 51 | "metadata", 52 | ], 53 | "": ["ast"], 54 | }, 55 | }, 56 | evmVersion: "istanbul", 57 | // viaIR: true, 58 | optimizer: { 59 | enabled: true, 60 | runs: 200, 61 | }, 62 | }, 63 | }, 64 | ], 65 | }, 66 | networks: { 67 | hardhat: { 68 | allowUnlimitedContractSize: true, 69 | allowBlocksWithSameTimestamp: true, 70 | }, 71 | zg: { 72 | ...userConfig, 73 | url: "https://evmrpc.0g.ai", 74 | }, 75 | zgTestnetStandard: { 76 | ...userConfig, 77 | url: "https://evmrpc-testnet.0g.ai", 78 | }, 79 | zgTestnetTurbo: { 80 | ...userConfig, 81 | url: "https://evmrpc-testnet.0g.ai/", 82 | }, 83 | zgTurbo: { 84 | ...userConfig, 85 | url: "https://evmrpc.0g.ai", 86 | }, 87 | zgStandard: { 88 | ...userConfig, 89 | url: "https://evmrpc.0g.ai", 90 | } 91 | }, 92 | namedAccounts: { 93 | deployer: 0, 94 | }, 95 | mocha: { 96 | timeout: 2000000, 97 | }, 98 | etherscan: { 99 | apiKey: { 100 | mainnet: ETHERSCAN_API_KEY || "", 101 | zgTurbo: "no-api-key-needed" 102 | }, 103 | customChains: [ 104 | { 105 | network: "zgTurbo", 106 | chainId: 16661, 107 | urls: { 108 | apiURL: "https://chainscan.0g.ai/open/api", 109 | browserURL: "https://chainscan.0g.ai" 110 | } 111 | } 112 | ] 113 | }, 114 | gasReporter: { 115 | currency: "Gwei", 116 | gasPrice: 10, 117 | enabled: process.env.REPORT_GAS ? true : false, 118 | }, 119 | abiExporter: { 120 | path: "./abis", 121 | runOnCompile: true, 122 | clear: true, 123 | flat: true, 124 | format: "json", 125 | }, 126 | }; 127 | if (NODE_URL && config.networks) { 128 | config.networks.custom = { 129 | ...userConfig, 130 | url: NODE_URL, 131 | }; 132 | } 133 | export default config; 134 | -------------------------------------------------------------------------------- /test/utils/mockMerkleTree.ts: -------------------------------------------------------------------------------- 1 | import { keccak } from "hash-wasm"; 2 | 3 | function bitLength(n: number): number { 4 | if (n === 0) { 5 | return 0; 6 | } 7 | return Math.floor(Math.log(n) / Math.log(2)) + 1; 8 | } 9 | 10 | function genLeafData(index: number): Buffer { 11 | const input = Array(256).fill(0); 12 | input[0] = index; 13 | return Buffer.from(input); 14 | } 15 | 16 | async function genLeaf(index: number): Promise { 17 | return Buffer.from(await keccak(genLeafData(index), 256), "hex"); 18 | } 19 | 20 | async function genLeaves(length: number): Promise { 21 | return await Promise.all(Array.from(new Array(length), (_, i) => genLeaf(i + 1))); 22 | } 23 | 24 | function pathIndex(input: string): number { 25 | let answer = 0; 26 | for (const x of input) { 27 | answer = 2 * answer + 1 + parseInt(x); 28 | } 29 | return answer; 30 | } 31 | 32 | class MockMerkle { 33 | leaves: Buffer[]; 34 | tree: Buffer[]; 35 | 36 | constructor(leaves: Buffer[]) { 37 | this.leaves = leaves; 38 | this.tree = []; 39 | } 40 | 41 | async build(): Promise { 42 | const length = this.leaves.length; 43 | const height = bitLength(length) + 1; 44 | const emptyLeaf = await genLeaf(0); 45 | 46 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 47 | const tree: Buffer[] = Array((1 << height) - 1); 48 | const offset = (1 << (height - 1)) - 1; 49 | tree[offset] = Buffer.from(Array(32).fill(0)); 50 | for (let i = 1; i < 1 << (height - 1); i++) { 51 | if (i - 1 < this.leaves.length) { 52 | tree[offset + i] = this.leaves[i - 1]; 53 | } else { 54 | tree[offset + i] = emptyLeaf; 55 | } 56 | } 57 | 58 | for (let h = height - 1; h > 0; h--) { 59 | const offset = (1 << (h - 1)) - 1; 60 | 61 | for (let i = 0; i < 1 << (h - 1); i++) { 62 | const left: number = 2 * (offset + i) + 1; 63 | const right: number = 2 * (offset + i) + 2; 64 | tree[offset + i] = Buffer.from(await keccak(Buffer.concat([tree[left], tree[right]]), 256), "hex"); 65 | } 66 | } 67 | 68 | this.tree = tree; 69 | return this; 70 | } 71 | 72 | at(input: string): Buffer { 73 | return this.tree[pathIndex(input)]; 74 | } 75 | 76 | root(): Buffer { 77 | return this.at(""); 78 | } 79 | 80 | height(): number { 81 | return Math.round(Math.log(this.tree.length + 1) / Math.log(2)); 82 | } 83 | 84 | slice(start: number, end: number): readonly Buffer[] { 85 | const offset = (1 << this.height()) / 2 - 1; 86 | return this.tree.slice(start + offset, end + offset); 87 | } 88 | 89 | length(): number { 90 | return this.leaves.length + 1; 91 | } 92 | 93 | proof(index: number): Buffer[] { 94 | const height = this.height() - 1; 95 | let indexString = ""; 96 | for (let i = 0; i < height; i++) { 97 | if (index % 2 === 0) { 98 | indexString = "0" + indexString; 99 | } else { 100 | indexString = "1" + indexString; 101 | } 102 | index = Math.floor(index / 2); 103 | } 104 | 105 | const sibling = function (path: string) { 106 | if (path[path.length - 1] === "0") { 107 | return path.slice(0, path.length - 1) + "1"; 108 | } else { 109 | return path.slice(0, path.length - 1) + "0"; 110 | } 111 | }; 112 | 113 | const proof: Buffer[] = []; 114 | 115 | for (let i = height; i > 0; i--) { 116 | proof.push(this.at(sibling(indexString.slice(0, i)))); 117 | } 118 | 119 | return proof; 120 | } 121 | 122 | getUnsealedData(recallPosition: number): Buffer[] { 123 | const unsealedData: Buffer[] = []; 124 | for (let i = 0; i < 16; i++) { 125 | const leafData = genLeafData(recallPosition + i); 126 | for (let j = 0; j < 256; j += 32) { 127 | unsealedData.push(leafData.subarray(j, j + 32)); 128 | } 129 | } 130 | return unsealedData; 131 | } 132 | } 133 | 134 | export { MockMerkle, genLeaf, genLeaves, genLeafData }; 135 | -------------------------------------------------------------------------------- /src/tasks/upgrade.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { task, types } from "hardhat/config"; 3 | import path from "path"; 4 | import { UpgradeableBeacon } from "../../typechain-types"; 5 | import { getConstructorArgs } from "../utils/constructor_args"; 6 | import { CONTRACTS, transact, validateError } from "../utils/utils"; 7 | import { getProxyInfo } from "./access"; 8 | 9 | task("upgrade", "upgrade contract") 10 | .addParam("name", "name of the proxy contract", undefined, types.string, false) 11 | .addParam("artifact", "name of the implementation contract", undefined, types.string, false) 12 | .addParam("execute", "settle transaction on chain", false, types.boolean, true) 13 | .setAction(async (taskArgs: { name: string; artifact: string; execute: boolean }, hre) => { 14 | const { deployments, getNamedAccounts } = hre; 15 | const { deployer } = await getNamedAccounts(); 16 | const beacon: UpgradeableBeacon = await hre.ethers.getContract(`${taskArgs.name}Beacon`, deployer); 17 | 18 | const result = await deployments.deploy(`${taskArgs.name}Impl`, { 19 | from: deployer, 20 | contract: taskArgs.artifact, 21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 22 | // @ts-expect-error 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access 24 | args: getConstructorArgs(hre.network.name, CONTRACTS[taskArgs.name].name), 25 | log: true, 26 | }); 27 | console.log(`new implementation deployed: ${result.address}`); 28 | 29 | await transact(beacon, "upgradeTo", [result.address], taskArgs.execute); 30 | }); 31 | 32 | task("upgrade:validate", "validate upgrade") 33 | .addParam("old", "name of the old contract", undefined, types.string, false) 34 | .addParam("new", "artifact of the new contract", undefined, types.string, false) 35 | .setAction(async (taskArgs: { old: string; new: string }, hre) => { 36 | const oldAddr = await (await hre.ethers.getContract(`${taskArgs.old}Impl`)).getAddress(); 37 | const newImpl = await hre.ethers.getContractFactory(taskArgs.new); 38 | const chainId = (await hre.ethers.provider.getNetwork()).chainId; 39 | const tmpFileName = `unknown-${chainId}.json`; 40 | const tmpFilePath = path.resolve(__dirname, `../../.openzeppelin/${tmpFileName}`); 41 | const fileName = `${hre.network.name}-${chainId}.json`; 42 | const filePath = path.resolve(__dirname, `../../.openzeppelin/${fileName}`); 43 | if (fs.existsSync(filePath)) { 44 | fs.copyFileSync(filePath, tmpFilePath); 45 | } else { 46 | throw Error(`network file ${filePath} not found!`); 47 | } 48 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 49 | // @ts-expect-error 50 | await hre.upgrades.validateUpgrade(oldAddr, newImpl, { 51 | unsafeAllow: ["constructor", "state-variable-immutable"], 52 | kind: "beacon", 53 | constructorArgs: getConstructorArgs(hre.network.name, taskArgs.new), 54 | }); 55 | fs.rmSync(tmpFilePath); 56 | }); 57 | 58 | task("upgrade:forceImportAll", "import contracts").setAction(async (_taskArgs, hre) => { 59 | const proxied = await getProxyInfo(hre); 60 | const chainId = (await hre.ethers.provider.getNetwork()).chainId; 61 | const tmpFileName = `unknown-${chainId}.json`; 62 | const tmpFilePath = path.resolve(__dirname, `../../.openzeppelin/${tmpFileName}`); 63 | if (fs.existsSync(tmpFilePath)) { 64 | console.log(`removing tmp network file ${tmpFilePath}..`); 65 | fs.rmSync(tmpFilePath); 66 | } 67 | for (const name of Array.from(proxied)) { 68 | const addr = await (await hre.ethers.getContract(`${name}Impl`)).getAddress(); 69 | const factory = await hre.ethers.getContractFactory(name); 70 | try { 71 | await hre.upgrades.forceImport(addr, factory, { 72 | kind: "beacon", 73 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 74 | // @ts-expect-error 75 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access 76 | constructorArgs: getConstructorArgs(hre.network.name, CONTRACTS[name].name), 77 | }); 78 | console.log(`force imported ${name}.`); 79 | } catch (e) { 80 | validateError(e, "The following deployment clashes with an existing one at"); 81 | console.log(`${name} already imported.`); 82 | } 83 | } 84 | if (fs.existsSync(tmpFilePath)) { 85 | const newFileName = `${hre.network.name}-${chainId}.json`; 86 | const newFilePath = path.resolve(__dirname, `../../.openzeppelin/${newFileName}`); 87 | console.log(`renaming tmp network file ${tmpFileName} to ${newFileName}..`); 88 | fs.renameSync(tmpFilePath, newFilePath); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /contracts/utils/Blake2b.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import "./UncheckedMath.sol"; 4 | 5 | library Blake2b { 6 | using UncheckedMath for uint; 7 | bytes32 public constant BLAKE2B_INIT_STATE0 = hex"48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5"; 8 | bytes32 public constant BLAKE2B_INIT_STATE1 = hex"d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b"; 9 | 10 | function blake2b(bytes32[2] memory input) internal view returns (bytes32[2] memory h) { 11 | h[0] = BLAKE2B_INIT_STATE0; 12 | h[1] = BLAKE2B_INIT_STATE1; 13 | 14 | h = blake2bF(h, input[0], input[1], bytes32(0x0), bytes32(0x0), 64, true); 15 | } 16 | 17 | function blake2b(bytes32[3] memory input) internal view returns (bytes32[2] memory h) { 18 | h[0] = BLAKE2B_INIT_STATE0; 19 | h[1] = BLAKE2B_INIT_STATE1; 20 | 21 | h = blake2bF(h, input[0], input[1], input[2], bytes32(0x0), 96, true); 22 | } 23 | 24 | function blake2b(bytes32[4] memory input) internal view returns (bytes32[2] memory h) { 25 | h[0] = BLAKE2B_INIT_STATE0; 26 | h[1] = BLAKE2B_INIT_STATE1; 27 | 28 | h = blake2bF(h, input[0], input[1], input[2], input[3], 128, true); 29 | } 30 | 31 | function blake2b(bytes32[5] memory input) internal view returns (bytes32[2] memory h) { 32 | h[0] = BLAKE2B_INIT_STATE0; 33 | h[1] = BLAKE2B_INIT_STATE1; 34 | 35 | h = blake2bF(h, input[0], input[1], input[2], input[3], 128, false); 36 | h = blake2bF(h, input[4], bytes32(0x0), bytes32(0x0), bytes32(0x0), 160, true); 37 | } 38 | 39 | function blake2b(bytes32[] memory input) internal view returns (bytes32[2] memory h) { 40 | h[0] = BLAKE2B_INIT_STATE0; 41 | h[1] = BLAKE2B_INIT_STATE1; 42 | if (input.length == 0) { 43 | h = blake2bF(h, bytes32(0x0), bytes32(0x0), bytes32(0x0), bytes32(0x0), 0, true); 44 | } 45 | for (uint i = 0; i < input.length; i += 4) { 46 | bytes32 m0 = input[i]; 47 | bytes32 m1 = bytes32(0x0); 48 | bytes32 m2 = bytes32(0x0); 49 | bytes32 m3 = bytes32(0x0); 50 | bool finalize = (i + 4 >= input.length); 51 | uint length = (i + 4) * 32; 52 | 53 | if (!finalize) { 54 | m1 = input[i + 1]; 55 | m2 = input[i + 2]; 56 | m3 = input[i + 3]; 57 | } else { 58 | length = input.length * 32; 59 | if (i + 1 < input.length) { 60 | m1 = input[i + 1]; 61 | if (i + 2 < input.length) { 62 | m2 = input[i + 2]; 63 | if (i + 3 < input.length) { 64 | m3 = input[i + 3]; 65 | } 66 | } 67 | } 68 | } 69 | 70 | h = blake2bF(h, m0, m1, m2, m3, length, finalize); 71 | } 72 | } 73 | 74 | function blake2bF( 75 | bytes32[2] memory h, 76 | bytes32 m0, 77 | bytes32 m1, 78 | bytes32 m2, 79 | bytes32 m3, 80 | uint offset, 81 | bool finalize 82 | ) internal view returns (bytes32[2] memory output) { 83 | uint32 rounds = 12; 84 | 85 | bytes8[2] memory t = blake2bLength(offset); 86 | bytes memory args = abi.encodePacked(rounds, h[0], h[1], m0, m1, m2, m3, t[0], t[1], finalize); 87 | 88 | uint blake2bOut; 89 | 90 | assembly { 91 | blake2bOut := staticcall(not(0), 0x09, add(args, 32), 0xd5, output, 0x40) 92 | } 93 | 94 | require(blake2bOut != 0, "blake2b internal error at blake2bF"); 95 | } 96 | 97 | function blake2bLength(uint length) internal pure returns (bytes8[2] memory t) { 98 | if (length < (1 << 16)) { 99 | t[0] = reverse16(uint64(length)); 100 | t[1] = bytes8(0x0); 101 | } else if (length < (1 << 32)) { 102 | t[0] = reverse32(uint64(length)); 103 | t[1] = bytes8(0x0); 104 | } else if (length < (1 << 64)) { 105 | t[0] = reverse64(uint64(length)); 106 | t[1] = bytes8(0x0); 107 | } else if (length < (1 << 128)) { 108 | uint64 lower = uint64(length & 0xFFFFFFFFFFFFFFFF); 109 | uint64 higher = uint64(length >> 64); 110 | t[0] = reverse64(lower); 111 | t[1] = reverse64(higher); 112 | } else { 113 | revert("blake2b input too long"); 114 | } 115 | } 116 | 117 | function reverse16(uint64 _v) internal pure returns (bytes8) { 118 | uint64 v = _v; 119 | // swap bytes 120 | v = (v >> 8) | (v << 8); 121 | v <<= 48; 122 | 123 | return bytes8(v); 124 | } 125 | 126 | function reverse32(uint64 _v) internal pure returns (bytes8) { 127 | uint64 v = _v; 128 | // swap bytes 129 | v = ((v & 0xFF00FF00) >> 8) | ((v & 0x00FF00FF) << 8); 130 | 131 | // swap 2-byte long pairs 132 | v = (v >> 16) | (v << 16); 133 | 134 | v <<= 32; 135 | 136 | return bytes8(v); 137 | } 138 | 139 | function reverse64(uint64 _v) internal pure returns (bytes8) { 140 | uint64 v = _v; 141 | // swap bytes 142 | v = ((v & 0xFF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF) << 8); 143 | 144 | // swap 2-byte long pairs 145 | v = ((v & 0xFFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF) << 16); 146 | 147 | // swap 4-byte long pairs 148 | v = (v >> 32) | (v << 32); 149 | 150 | return bytes8(v); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/tasks/access.ts: -------------------------------------------------------------------------------- 1 | import { task, types } from "hardhat/config"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | import { AccessControl, AccessControlEnumerable, UpgradeableBeacon } from "../../typechain-types"; 4 | import { 5 | AnyContractMeta, 6 | CONTRACTS, 7 | DEFAULT_ADMIN_ROLE, 8 | getTypedContract, 9 | PAUSER_ROLE, 10 | validateError, 11 | } from "../utils/utils"; 12 | 13 | export async function getProxyInfo(hre: HardhatRuntimeEnvironment) { 14 | const proxied = new Set(); 15 | for (const contractMeta of Object.values(CONTRACTS)) { 16 | const name = contractMeta.name; 17 | try { 18 | await hre.ethers.getContract(`${name}Beacon`); 19 | proxied.add(name); 20 | } catch (e) { 21 | validateError(e, "No Contract deployed with name"); 22 | } 23 | } 24 | return proxied; 25 | } 26 | 27 | task("access:upgrade", "transfer beacon ownership to timelock") 28 | .addParam("timelock", "timelock address", undefined, types.string, false) 29 | .setAction(async (taskArgs: { timelock: string }, hre) => { 30 | const { getNamedAccounts } = hre; 31 | const { deployer } = await getNamedAccounts(); 32 | const proxied = await getProxyInfo(hre); 33 | for (const name of Array.from(proxied)) { 34 | const beacon: UpgradeableBeacon = await hre.ethers.getContract(`${name}Beacon`, deployer); 35 | if ((await beacon.owner()).toLowerCase() === deployer.toLowerCase()) { 36 | console.log(`transfer ownership of ${name}Beacon..`); 37 | await (await beacon.transferOwnership(taskArgs.timelock)).wait(); 38 | } 39 | } 40 | }); 41 | 42 | task("access:admin", "grant default admin role to timelock") 43 | .addParam("timelock", "timelock address", undefined, types.string, false) 44 | .setAction(async (taskArgs: { timelock: string }, hre) => { 45 | const { getNamedAccounts } = hre; 46 | const { deployer } = await getNamedAccounts(); 47 | for (const contractMeta of Object.values(CONTRACTS)) { 48 | const name = contractMeta.name; 49 | let contract: AccessControl; 50 | try { 51 | const anyContract = await getTypedContract(hre, contractMeta as AnyContractMeta); 52 | if (!anyContract.interface.hasFunction("DEFAULT_ADMIN_ROLE")) { 53 | continue; 54 | } 55 | contract = anyContract as AccessControl; 56 | } catch (e) { 57 | validateError(e, "No Contract deployed with name"); 58 | continue; 59 | } 60 | if (await contract.hasRole(DEFAULT_ADMIN_ROLE, deployer)) { 61 | console.log(`granting default admin role of ${name} to timelock..`); 62 | await (await contract.grantRole(DEFAULT_ADMIN_ROLE, taskArgs.timelock)).wait(); 63 | } else { 64 | console.log(`deployer does not have default admin role of ${name}, skip.`); 65 | } 66 | } 67 | }); 68 | 69 | task("access:pauser", "grant pauser role to multisig") 70 | .addParam("multisig", "multisig address", undefined, types.string, false) 71 | .setAction(async (taskArgs: { multisig: string }, hre) => { 72 | const { getNamedAccounts } = hre; 73 | const { deployer } = await getNamedAccounts(); 74 | for (const contractMeta of Object.values(CONTRACTS)) { 75 | const name = contractMeta.name; 76 | let contract: AccessControl; 77 | try { 78 | const anyContract = await getTypedContract(hre, contractMeta as AnyContractMeta); 79 | if (!anyContract.interface.hasFunction("PAUSER_ROLE")) { 80 | continue; 81 | } 82 | contract = anyContract as AccessControl; 83 | } catch (e) { 84 | validateError(e, "No Contract deployed with name"); 85 | continue; 86 | } 87 | if (await contract.hasRole(DEFAULT_ADMIN_ROLE, deployer)) { 88 | console.log(`granting pauser role of ${name} to multisig..`); 89 | await (await contract.grantRole(PAUSER_ROLE, taskArgs.multisig)).wait(); 90 | } else { 91 | console.log(`deployer does not have default admin role of ${name}, skip.`); 92 | } 93 | } 94 | }); 95 | 96 | task("revoke:admin", "revoke admin role").setAction(async (_taskArgs, hre) => { 97 | const { getNamedAccounts } = hre; 98 | const { deployer } = await getNamedAccounts(); 99 | for (const contractMeta of Object.values(CONTRACTS)) { 100 | const name = contractMeta.name; 101 | let contract: AccessControlEnumerable; 102 | try { 103 | const anyContract = await getTypedContract(hre, contractMeta as AnyContractMeta); 104 | if (!anyContract.interface.hasFunction("DEFAULT_ADMIN_ROLE")) { 105 | continue; 106 | } 107 | contract = anyContract as AccessControlEnumerable; 108 | } catch (e) { 109 | validateError(e, "No Contract deployed with name"); 110 | continue; 111 | } 112 | if (await contract.hasRole(DEFAULT_ADMIN_ROLE, deployer)) { 113 | if ((await contract.getRoleMemberCount(DEFAULT_ADMIN_ROLE)) === 1n) { 114 | console.log(`deployer is the only admin of ${name}, skip.`); 115 | continue; 116 | } 117 | console.log(`renouncing deployer's default admin role of ${name}..`); 118 | await (await contract.renounceRole(DEFAULT_ADMIN_ROLE, deployer)).wait(); 119 | } else { 120 | console.log(`deployer does not have default admin role of ${name}, skip.`); 121 | } 122 | } 123 | }); 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 0G Storage Contract 2 | 3 | ## Introduction 4 | 5 | The 0g-storage-contracts system is tasked with the management of data submission for 0g storage, Proof of Random Access (PoRA) mining, and the economic model associated with these processes. It features a modular design that allows for various implementations of its components, including: 6 | 7 | ### Flow 8 | 9 | The Flow module is responsible for receiving user storage requests along with their metadata, maintaining the overall storage Merkle root, and generating mining tasks in period. 10 | 11 | ### Market 12 | 13 | Upon receiving a submission from a user, the Flow module notifies the Market contract. If the Market contract determines that the user has not paid sufficient fees, it will revert the entire transaction. 14 | 15 | It is important to note that the payment of fees and the submission of data are independent steps. The Flow does not automatically pay the Market when submitting data. This design allows for more flexible Market logic, such as multi-currency payments. Some contracts, like FixedPriceFlow, merge these two steps into a single transaction. 16 | 17 | Implementations of the Market include: 18 | - **FixedPrice**: Charges a fixed price for submissions. Users can voluntarily pay a tip to incentivize more miners for storage. 19 | 20 | ### Reward 21 | 22 | The Market can transfer a portion of the balance to the Reward module and inform it about which segment of data the fees are for. 23 | 24 | Besides, after a mining task is verified, the Flow informs the Reward about the pricing chunk hit by the miner's PoRA and requests the reward. The specific amount of the reward is determined by the Reward contract. 25 | 26 | Implementations of the Reward include: 27 | - **ChunkDecayReward**: The earliest economic model design, where each pricing chunk has a separate reward pool composed of various fees paid by users during data submission. The fees are released at a half-life rate of 25 years, allowing miners to extract half of the remaining unlocked rewards with each mining hit. 28 | - **ChunkLinearReward**: A variation of the previous design that changes the release method to a fixed-time linear release, aiming to address the issue of low capital utilization. 29 | - **OnePoolReward**: Maintains a single reward pool within a mining time window (e.g., only data submitted in the last three months is eligible for mining). This design focuses on a singular, consolidated reward pool for the specified time window. 30 | 31 | ### Mine 32 | 33 | Responsible for validating mining submissions and distributing rewards. 34 | 35 | ## Compile 36 | 37 | ```shell 38 | yarn 39 | yarn build 40 | ``` 41 | 42 | ## Deploy contract 43 | 44 | If the economic model is not enabled, data submissions and mining will not involve token transfers. If enabled, `FixedPrice` will be selected as the market, and `OnePoolReward` as the Reward. 45 | 46 | Use the following command for deployment with economic model: 47 | 48 | ``` 49 | yarn deploy-market-enabled 50 | ``` 51 | 52 | For deployment without economic model: 53 | 54 | ``` 55 | yarn deploy-no-market 56 | ``` 57 | 58 | To export all contract addresses of a deployment into a single file: 59 | ``` 60 | yarn hardhat export --network --export 61 | ``` 62 | 63 | ### Deployment Configurations 64 | 65 | You can create a custom configuration file for a network under [networks](src/networks/) folder, then put the network name and the configuration struct as entry in `GlobalConfig` mapping in [config.ts](src/config.ts). 66 | 67 | See configuration for [zg](src/networks/zerog_contract_config.ts) network as reference. 68 | 69 | ### targetnetwork 70 | 71 | You have several options for the target network, which you can modify in `hardhat.config.ts`: 72 | 73 | - **localtest**: For local testing environments. 74 | - **zg**: For deploying on Zero Gravity network. 75 | 76 | When deploying, ensure that your environment is properly configured with the necessary variables and network settings to match your deployment goals. This modular and flexible approach allows for tailored deployments that fit the specific needs of your project. 77 | 78 | ## Standard Operating Procedure for Contract Deployment/Upgrade 79 | 80 | In this section, we will describe how to maintain information related to contract deployment and upgrades. 81 | 82 | ### Deployments 83 | 84 | After the first contract deployment following the above procedure, a folder named after the corresponding network will be created in the `deployments/` directory under the root. This folder will contain all the information about the deployed contracts, including their addresses and ABI, and each time a new contract is deployed on this network, the deployments will also be updated. The deployments need to be properly preserved and updated, such as by setting up a separate Git repository. 85 | 86 | ### Upgrade 87 | 88 | We use the Hardhat task defined in [upgrade](src/tasks/upgrade.ts) to perform contract upgrades and maintenance. 89 | 90 | First, after each contract deployment or upgrade, we need to execute the `upgrade:forceImportAll` task to persist the current version of the contract locally. Note that when executing the force import, you must ensure that the locally generated artifacts correspond to the version of the contract. The generated version files are saved in `.openzeppelin/`. 91 | 92 | Then, when we modify a contract and want to upgrade, we need to check for conflicts between the new contract and the previously deployed version (such as incompatible storage layout). We can use `upgrade:validate` to validate it. Similarly, this step needs to ensure that the local artifacts are compiled from the new version of the contract. 93 | 94 | Finally, after completing the above checks, you can execute the contract upgrade transaction using the `upgrade` task. Once the upgrade is complete, we have deployed a new implementation contract, so don't forget to execute `forceImportAll` to update the local contract deployment version information and save the updated `deployments/` folder. 95 | 96 | ### Overall Workflow 97 | 98 | 1. First contract deployments; 99 | 2. Execute `forceImportAll`, save the generated version files and `deployments/`; 100 | 3. Prepare an upgrade; 101 | 4. Compile contracts, validate upgrade and then execute; 102 | 5. Execute `forceImportAll`, save the modified version files and `deployments/`; 103 | 6. repeat 3 to 5. -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { FACTORY_POSTFIX } from "@typechain/ethers-v6/dist/common"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | 4 | // We use the Typechain factory class objects to fill the `CONTRACTS` mapping. These objects are used 5 | // by hardhat-deploy to locate compiled contract artifacts. However, an exception occurs if we import 6 | // from Typechain files before they are generated. To avoid this, we follow a two-step process: 7 | // 8 | // 1. We import the types at compile time to ensure type safety. Hardhat does not report an error even 9 | // if these files are not yet generated, as long as the "--typecheck" command-line argument is not used. 10 | import { BaseContract, ContractFactory, ContractRunner, ethers, Signer } from "ethers"; 11 | import * as TypechainTypes from "../../typechain-types"; 12 | // 2. We import the values at runtime and silently ignore any exceptions. 13 | export let Factories = {} as typeof TypechainTypes; 14 | try { 15 | // eslint-disable-next-line @typescript-eslint/no-var-requires 16 | Factories = require("../../typechain-types") as typeof TypechainTypes; 17 | } catch (err) { 18 | // ignore 19 | } 20 | 21 | interface TypechainFactory { 22 | new (...args: ConstructorParameters): ContractFactory; 23 | connect: (address: string, runner?: ContractRunner | null) => T; 24 | } 25 | 26 | class ContractMeta { 27 | factory: TypechainFactory; 28 | /** Deployment name */ 29 | name: string; 30 | 31 | constructor(factory: TypechainFactory, name?: string) { 32 | this.factory = factory; 33 | this.name = name ?? this.contractName(); 34 | } 35 | 36 | contractName() { 37 | // this.factory is undefined when the typechain files are not generated yet 38 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 39 | return this.factory?.name.slice(0, -FACTORY_POSTFIX.length); 40 | } 41 | } 42 | 43 | export const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; 44 | export const PAUSER_ROLE = ethers.id("PAUSER_ROLE"); 45 | 46 | export const CONTRACTS = { 47 | FixedPriceFlow: new ContractMeta(Factories.FixedPriceFlow__factory), 48 | Flow: new ContractMeta(Factories.Flow__factory), 49 | FixedPrice: new ContractMeta(Factories.FixedPrice__factory), 50 | PoraMine: new ContractMeta(Factories.PoraMine__factory), 51 | PoraMineTest: new ContractMeta(Factories.PoraMineTest__factory), 52 | DummyMarket: new ContractMeta(Factories.DummyMarket__factory), 53 | DummyReward: new ContractMeta(Factories.DummyReward__factory), 54 | Blake2bTest: new ContractMeta(Factories.Blake2bTest__factory), 55 | ChunkDecayReward: new ContractMeta(Factories.ChunkDecayReward__factory), 56 | ChunkLinearReward: new ContractMeta(Factories.ChunkLinearReward__factory), 57 | } as const; 58 | 59 | type GetContractTypeFromContractMeta = F extends ContractMeta ? C : never; 60 | 61 | type AnyContractType = GetContractTypeFromContractMeta<(typeof CONTRACTS)[keyof typeof CONTRACTS]>; 62 | 63 | export type AnyContractMeta = ContractMeta; 64 | 65 | const UPGRADEABLE_BEACON = "UpgradeableBeacon"; 66 | const BEACON_PROXY = "BeaconProxy"; 67 | 68 | export async function deployDirectly( 69 | hre: HardhatRuntimeEnvironment, 70 | contract: ContractMeta, 71 | args: unknown[] = [] 72 | ) { 73 | const { deployments, getNamedAccounts } = hre; 74 | const { deployer } = await getNamedAccounts(); 75 | // deploy implementation 76 | await deployments.deploy(contract.name, { 77 | from: deployer, 78 | contract: contract.contractName(), 79 | args: args, 80 | log: true, 81 | }); 82 | } 83 | 84 | export async function deployInBeaconProxy( 85 | hre: HardhatRuntimeEnvironment, 86 | contract: ContractMeta, 87 | args: unknown[] = [] 88 | ) { 89 | const { deployments, getNamedAccounts } = hre; 90 | const { deployer } = await getNamedAccounts(); 91 | // deploy implementation 92 | await deployments.deploy(`${contract.name}Impl`, { 93 | from: deployer, 94 | contract: contract.contractName(), 95 | args: args, 96 | log: true, 97 | }); 98 | const implementation = await hre.ethers.getContract(`${contract.name}Impl`); 99 | // deploy beacon 100 | await deployments.deploy(`${contract.name}Beacon`, { 101 | from: deployer, 102 | contract: UPGRADEABLE_BEACON, 103 | args: [await implementation.getAddress(), deployer], 104 | log: true, 105 | }); 106 | const beacon = await hre.ethers.getContract(`${contract.name}Beacon`); 107 | // deploy proxy 108 | await deployments.deploy(contract.name, { 109 | from: deployer, 110 | contract: BEACON_PROXY, 111 | args: [await beacon.getAddress(), []], 112 | log: true, 113 | }); 114 | } 115 | 116 | export async function getTypedContract( 117 | hre: HardhatRuntimeEnvironment, 118 | contract: ContractMeta, 119 | signer?: Signer | string 120 | ) { 121 | const address = await (await hre.ethers.getContract(contract.name)).getAddress(); 122 | if (signer === undefined) { 123 | signer = (await hre.getNamedAccounts()).deployer; 124 | } 125 | if (typeof signer === "string") { 126 | signer = await hre.ethers.getSigner(signer); 127 | } 128 | return contract.factory.connect(address, signer); 129 | } 130 | 131 | export async function transact(contract: BaseContract, methodName: string, params: unknown[], execute: boolean) { 132 | if (execute) { 133 | await (await contract.getFunction(methodName).send(...params)).wait(); 134 | } else { 135 | console.log(`to: ${await contract.getAddress()}`); 136 | console.log(`func: ${contract.interface.getFunction(methodName)?.format()}`); 137 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 138 | console.log(`params: ${JSON.stringify(params, (_, v) => (typeof v === "bigint" ? v.toString() : v))}`); 139 | console.log(`data: ${contract.interface.encodeFunctionData(methodName, params)}`); 140 | } 141 | } 142 | 143 | export function validateError(e: unknown, msg: string) { 144 | if (e instanceof Error) { 145 | if (!e.toString().includes(msg)) { 146 | throw Error(`unexpected error: ${String(e)}`); 147 | } 148 | } else { 149 | throw Error(`unexpected error: ${String(e)}`); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /test/reward.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Signer } from "ethers"; 3 | import hre, { deployments, ethers } from "hardhat"; 4 | import { CONTRACTS, getTypedContract } from "../src/utils/utils"; 5 | import { ChunkLinearReward } from "../typechain-types"; 6 | 7 | describe("Chunk Reward Foundation Admin", function () { 8 | let chunkReward: ChunkLinearReward; 9 | let deployer: Signer; 10 | let foundationAdmin: Signer; 11 | let user: Signer; 12 | let newAdmin: Signer; 13 | let treasury: Signer; 14 | 15 | before(async function () { 16 | await deployments.fixture(["market-enabled"]); 17 | 18 | [deployer, foundationAdmin, user, newAdmin, treasury] = await ethers.getSigners(); 19 | 20 | chunkReward = await getTypedContract(hre, CONTRACTS.ChunkLinearReward); 21 | }); 22 | 23 | describe("Foundation Admin Access Control", function () { 24 | it("should not allow non-foundation admin to call admin functions", async function () { 25 | const newFeeRate = 500; // 5% 26 | const treasuryAddress = await treasury.getAddress(); 27 | const baseRewardAmount = ethers.parseEther("1.0"); 28 | const newAdminAddress = await newAdmin.getAddress(); 29 | 30 | // Test service fee rate 31 | await expect(chunkReward.connect(user).setServiceFeeRate(newFeeRate)).to.be.revertedWith( 32 | "Not foundation admin" 33 | ); 34 | 35 | await expect(chunkReward.connect(deployer).setServiceFeeRate(newFeeRate)).to.be.revertedWith( 36 | "Not foundation admin" 37 | ); 38 | 39 | // Test treasury address 40 | await expect(chunkReward.connect(user).setTreasury(treasuryAddress)).to.be.revertedWith( 41 | "Not foundation admin" 42 | ); 43 | 44 | await expect(chunkReward.connect(deployer).setTreasury(treasuryAddress)).to.be.revertedWith( 45 | "Not foundation admin" 46 | ); 47 | 48 | // Test base reward 49 | await expect(chunkReward.connect(user).setBaseReward(baseRewardAmount)).to.be.revertedWith( 50 | "Not foundation admin" 51 | ); 52 | 53 | await expect(chunkReward.connect(deployer).setBaseReward(baseRewardAmount)).to.be.revertedWith( 54 | "Not foundation admin" 55 | ); 56 | 57 | // Test foundation admin transfer 58 | await expect(chunkReward.connect(user).setFoundationAdmin(newAdminAddress)).to.be.revertedWith( 59 | "Not foundation admin" 60 | ); 61 | 62 | await expect(chunkReward.connect(deployer).setFoundationAdmin(newAdminAddress)).to.be.revertedWith( 63 | "Not foundation admin" 64 | ); 65 | }); 66 | }); 67 | 68 | describe("Foundation Admin Functions (with mock admin)", function () { 69 | let testChunkReward: ChunkLinearReward; 70 | 71 | beforeEach(async function () { 72 | // Deploy a fresh contract instance with our mock foundation admin 73 | const ChunkLinearRewardFactory = await ethers.getContractFactory("ChunkLinearReward"); 74 | testChunkReward = await ChunkLinearRewardFactory.deploy(3 * 31 * 86400); // 3 months 75 | await testChunkReward.waitForDeployment(); 76 | 77 | // Get market and mine addresses from the existing deployment 78 | const marketAddress = await chunkReward.market(); 79 | const mineAddress = await chunkReward.mine(); 80 | 81 | // Initialize with our mock foundation admin 82 | await testChunkReward.initialize(marketAddress, mineAddress, await foundationAdmin.getAddress()); 83 | }); 84 | 85 | it("should allow foundation admin to set all admin parameters", async function () { 86 | const newFeeRate = 750; // 7.5% 87 | const treasuryAddress = await treasury.getAddress(); 88 | const baseRewardAmount = ethers.parseEther("2.0"); 89 | 90 | // Test service fee rate 91 | await expect(testChunkReward.connect(foundationAdmin).setServiceFeeRate(newFeeRate)).to.not.be.reverted; 92 | expect(await testChunkReward.serviceFeeRateBps()).to.equal(newFeeRate); 93 | 94 | // Test treasury address 95 | await expect(testChunkReward.connect(foundationAdmin).setTreasury(treasuryAddress)).to.not.be.reverted; 96 | expect(await testChunkReward.treasury()).to.equal(treasuryAddress); 97 | 98 | // Test base reward 99 | await expect(testChunkReward.connect(foundationAdmin).setBaseReward(baseRewardAmount)).to.not.be.reverted; 100 | expect(await testChunkReward.baseReward()).to.equal(baseRewardAmount); 101 | }); 102 | 103 | it("should allow foundation admin to transfer foundation admin role", async function () { 104 | const newAdminAddress = await newAdmin.getAddress(); 105 | 106 | // Transfer foundation admin role 107 | await expect(testChunkReward.connect(foundationAdmin).setFoundationAdmin(newAdminAddress)).to.not.be 108 | .reverted; 109 | 110 | // Old admin should no longer have access 111 | await expect(testChunkReward.connect(foundationAdmin).setServiceFeeRate(400)).to.be.revertedWith( 112 | "Not foundation admin" 113 | ); 114 | 115 | // New admin should have access 116 | await expect(testChunkReward.connect(newAdmin).setServiceFeeRate(400)).to.not.be.reverted; 117 | 118 | expect(await testChunkReward.serviceFeeRateBps()).to.equal(400); 119 | }); 120 | }); 121 | 122 | describe("Base Reward Functionality", function () { 123 | it("should allow anyone to donate to base reward pool", async function () { 124 | const donationAmount = ethers.parseEther("5.0"); 125 | const initialBaseReward = await chunkReward.totalBaseReward(); 126 | 127 | await expect(chunkReward.connect(user).donate({ value: donationAmount })).to.not.be.reverted; 128 | 129 | expect(await chunkReward.totalBaseReward()).to.equal(initialBaseReward + donationAmount); 130 | }); 131 | 132 | it("should allow multiple donations to accumulate", async function () { 133 | const donation1 = ethers.parseEther("1.0"); 134 | const donation2 = ethers.parseEther("2.0"); 135 | const initialBaseReward = await chunkReward.totalBaseReward(); 136 | 137 | await chunkReward.connect(user).donate({ value: donation1 }); 138 | await chunkReward.connect(deployer).donate({ value: donation2 }); 139 | 140 | expect(await chunkReward.totalBaseReward()).to.equal(initialBaseReward + donation1 + donation2); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/tasks/reward.ts: -------------------------------------------------------------------------------- 1 | import { task, types } from "hardhat/config"; 2 | import { CONTRACTS, getTypedContract } from "../utils/utils"; 3 | import { UpgradeableBeacon } from "../../typechain-types"; 4 | 5 | task("reward:donate", "set extra total base reward") 6 | .addParam("amnt", "amount of donation", undefined, types.string, false) 7 | .setAction(async (taskArgs: { amnt: string }, hre) => { 8 | const reward = await getTypedContract(hre, CONTRACTS.ChunkLinearReward); 9 | await (await reward.donate({ value: hre.ethers.parseEther(taskArgs.amnt) })).wait(); 10 | console.log(`donated ${taskArgs.amnt}`); 11 | }); 12 | 13 | task("reward:setBaseReward", "set extra base reward") 14 | .addParam("amnt", "amount of base reward", undefined, types.string, false) 15 | .setAction(async (taskArgs: { amnt: string }, hre) => { 16 | const reward = await getTypedContract(hre, CONTRACTS.ChunkLinearReward); 17 | await (await reward.setBaseReward(hre.ethers.parseEther(taskArgs.amnt))).wait(); 18 | console.log(`set base reward to ${taskArgs.amnt}`); 19 | }); 20 | 21 | task("reward:beacon-owner", "check beacon contract owner") 22 | .setAction(async (_taskArgs, hre) => { 23 | const beaconContract = await hre.ethers.getContract("ChunkLinearRewardBeacon"); 24 | const beacon = beaconContract as UpgradeableBeacon; 25 | const beaconAddress = await beacon.getAddress(); 26 | 27 | const owner = await beacon.owner(); 28 | console.log(`Beacon contract: ${beaconAddress}`); 29 | console.log(`Current owner: ${owner}`); 30 | 31 | const [signer] = await hre.ethers.getSigners(); 32 | const signerAddress = await signer.getAddress(); 33 | console.log(`Your address: ${signerAddress}`); 34 | console.log(`You are the owner: ${owner.toLowerCase() === signerAddress.toLowerCase()}`); 35 | }); 36 | 37 | task("reward:transfer-beacon-ownership", "transfer beacon contract ownership") 38 | .addParam("newOwner", "new owner address", undefined, types.string, false) 39 | .addParam("execute", "execute the transfer", false, types.boolean, true) 40 | .setAction(async (taskArgs: { newOwner: string; execute: boolean }, hre) => { 41 | const beaconContract = await hre.ethers.getContract("ChunkLinearRewardBeacon"); 42 | const beacon = beaconContract as UpgradeableBeacon; 43 | 44 | const currentOwner = await beacon.owner(); 45 | console.log(`Current owner: ${currentOwner}`); 46 | console.log(`New owner: ${taskArgs.newOwner}`); 47 | 48 | if (taskArgs.execute) { 49 | console.log("Transferring beacon ownership..."); 50 | const tx = await beacon.transferOwnership(taskArgs.newOwner); 51 | await tx.wait(); 52 | console.log(`Ownership transferred! Transaction hash: ${tx.hash}`); 53 | 54 | const newOwner = await beacon.owner(); 55 | console.log(`New owner confirmed: ${newOwner}`); 56 | } else { 57 | console.log("Dry run complete. Use --execute true to perform the transfer."); 58 | console.log(`Transfer call: beacon.transferOwnership("${taskArgs.newOwner}")`); 59 | } 60 | }); 61 | 62 | task("reward:upgrade", "Deploy new ChunkLinearReward implementation and upgrade beacon") 63 | .addOptionalParam("key", "Private key to use (defaults to deployer)", undefined, types.string) 64 | .setAction(async (taskArgs: { key?: string }, hre) => { 65 | console.log(`🔄 Upgrading ChunkLinearReward contract on network: ${hre.network.name}`); 66 | 67 | let signer; 68 | if (taskArgs.key) { 69 | signer = new hre.ethers.Wallet(taskArgs.key, hre.ethers.provider); 70 | console.log(`Using provided private key, signer address: ${await signer.getAddress()}`); 71 | } else { 72 | const { deployer } = await hre.getNamedAccounts(); 73 | signer = await hre.ethers.getSigner(deployer); 74 | console.log(`Using deployer address: ${deployer}`); 75 | } 76 | 77 | try { 78 | // Get the beacon contract with proper typing 79 | const beaconContract = await hre.ethers.getContract("ChunkLinearRewardBeacon"); 80 | const beacon = beaconContract as UpgradeableBeacon; 81 | const beaconAddress = await beacon.getAddress(); 82 | console.log(`📡 Beacon contract address: ${beaconAddress}`); 83 | 84 | // Get current implementation 85 | const currentImpl = await beacon.implementation(); 86 | console.log(`📋 Current implementation: ${currentImpl}`); 87 | 88 | // Check if signer is the owner of the beacon 89 | const owner = await beacon.owner(); 90 | const signerAddress = await signer.getAddress(); 91 | 92 | if (owner.toLowerCase() !== signerAddress.toLowerCase()) { 93 | throw new Error(`❌ Signer ${signerAddress} is not the owner of the beacon. Owner is: ${owner}`); 94 | } 95 | 96 | console.log("🏗️ Deploying new implementation..."); 97 | 98 | // Deploy new implementation with hardhat-deploy 99 | const { deployments } = hre; 100 | const releaseSeconds = "32140800"; // Keep the same releaseSeconds 101 | 102 | const result = await deployments.deploy(`ChunkLinearRewardImpl`, { 103 | from: await signer.getAddress(), 104 | contract: CONTRACTS.ChunkLinearReward.contractName(), 105 | args: [releaseSeconds], 106 | log: true, 107 | }); 108 | 109 | const newImplAddress = result.address; 110 | console.log(`🆕 New implementation deployed: ${newImplAddress}`); 111 | 112 | if (currentImpl.toLowerCase() === newImplAddress.toLowerCase()) { 113 | console.log("⚠️ Implementation is already up to date!"); 114 | return; 115 | } 116 | 117 | console.log("🚀 Sending upgrade transaction..."); 118 | const tx = await beacon.connect(signer).upgradeTo(newImplAddress); 119 | console.log(`📝 Transaction hash: ${tx.hash}`); 120 | 121 | const receipt = await tx.wait(); 122 | if (!receipt) { 123 | throw new Error("Transaction receipt is null"); 124 | } 125 | 126 | console.log(`✅ Upgrade completed in block: ${receipt.blockNumber}`); 127 | console.log(`⛽ Gas used: ${receipt.gasUsed.toString()}`); 128 | 129 | // Verify the upgrade 130 | const updatedImpl = await beacon.implementation(); 131 | console.log(`🔍 Verified new implementation: ${updatedImpl}`); 132 | 133 | if (updatedImpl.toLowerCase() === newImplAddress.toLowerCase()) { 134 | console.log("🎉 ChunkLinearReward contract successfully upgraded!"); 135 | console.log(`📋 Old implementation: ${currentImpl}`); 136 | console.log(`🆕 New implementation: ${newImplAddress}`); 137 | } else { 138 | console.log("❌ Upgrade verification failed!"); 139 | } 140 | 141 | } catch (error) { 142 | console.error("❌ Upgrade failed:", error); 143 | throw error; 144 | } 145 | }); 146 | 147 | -------------------------------------------------------------------------------- /contracts/reward/ChunkRewardBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | 3 | pragma solidity >=0.8.0 <0.9.0; 4 | 5 | import "../utils/ZgsSpec.sol"; 6 | import "../utils/OnlySender.sol"; 7 | import "../interfaces/IReward.sol"; 8 | 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | import "@openzeppelin/contracts/utils/Address.sol"; 11 | 12 | import "./Reward.sol"; 13 | import "../utils/PullPayment.sol"; 14 | 15 | import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; 16 | 17 | abstract contract ChunkRewardBase is IReward, PullPayment, AccessControlEnumerableUpgradeable { 18 | using RewardLibrary for Reward; 19 | 20 | modifier onlyFoundationAdmin() { 21 | require(_msgSender() == _getChunkRewardBaseStorage().foundationAdmin, "Not foundation admin"); 22 | _; 23 | } 24 | 25 | /// @custom:storage-location erc7201:0g.storage.ChunkRewardBase 26 | struct ChunkRewardBaseStorage { 27 | address market; 28 | address mine; 29 | mapping(uint => Reward) rewards; 30 | uint totalBaseReward; 31 | uint baseReward; 32 | uint serviceFeeRateBps; 33 | address treasury; 34 | address foundationAdmin; 35 | } 36 | 37 | // keccak256(abi.encode(uint(keccak256("0g.storage.ChunkRewardBase")) - 1)) & ~bytes32(uint(0xff)) 38 | bytes32 private constant ChunkRewardBaseStorageLocation = 39 | 0x5c8dfb41bf775ed78439bdc545dd2d846bd8da274c69de26cd754e645898d800; 40 | 41 | function _getChunkRewardBaseStorage() private pure returns (ChunkRewardBaseStorage storage $) { 42 | assembly { 43 | $.slot := ChunkRewardBaseStorageLocation 44 | } 45 | } 46 | 47 | function initialize(address market_, address mine_, address foundationAdmin_) public initializer { 48 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 49 | _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); 50 | 51 | $.market = market_; 52 | $.mine = mine_; 53 | $.foundationAdmin = foundationAdmin_; 54 | 55 | // deploy pullpayment escrow 56 | __PullPayment_init(); 57 | } 58 | 59 | /*=== view ===*/ 60 | 61 | function market() public view returns (address) { 62 | return _getChunkRewardBaseStorage().market; 63 | } 64 | 65 | function mine() public view returns (address) { 66 | return _getChunkRewardBaseStorage().mine; 67 | } 68 | 69 | function rewards(uint id) public view returns (Reward memory) { 70 | return _getChunkRewardBaseStorage().rewards[id]; 71 | } 72 | 73 | function totalBaseReward() public view returns (uint) { 74 | return _getChunkRewardBaseStorage().totalBaseReward; 75 | } 76 | 77 | function baseReward() public view returns (uint) { 78 | return _getChunkRewardBaseStorage().baseReward; 79 | } 80 | 81 | function serviceFeeRateBps() public view returns (uint) { 82 | return _getChunkRewardBaseStorage().serviceFeeRateBps; 83 | } 84 | 85 | function treasury() public view returns (address) { 86 | return _getChunkRewardBaseStorage().treasury; 87 | } 88 | 89 | function foundationAdmin() public view returns (address) { 90 | return _getChunkRewardBaseStorage().foundationAdmin; 91 | } 92 | 93 | /*=== main ===*/ 94 | 95 | function fillReward(uint beforeLength, uint chargedSectors) external payable { 96 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 97 | 98 | require(_msgSender() == $.market, "Sender does not have permission"); 99 | 100 | uint serviceFee = (msg.value * $.serviceFeeRateBps) / 10000; 101 | if (serviceFee > 0) { 102 | Address.sendValue(payable($.treasury), serviceFee); 103 | } 104 | uint restFee = msg.value - serviceFee; 105 | 106 | uint totalSectors = chargedSectors; 107 | uint feePerPricingChunk = (restFee * SECTORS_PER_PRICE) / totalSectors; 108 | uint afterLength = beforeLength + totalSectors; 109 | 110 | uint firstPricingLength = SECTORS_PER_PRICE - (beforeLength % SECTORS_PER_PRICE); 111 | uint firstPricingIndex = (beforeLength + firstPricingLength) / SECTORS_PER_PRICE - 1; 112 | 113 | uint lastPricingLength = ((afterLength - 1) % SECTORS_PER_PRICE) + 1; 114 | uint lastPricingIndex = (afterLength - lastPricingLength) / SECTORS_PER_PRICE; 115 | 116 | bool finalizeLastChunk = (afterLength == (lastPricingIndex + 1) * SECTORS_PER_PRICE); 117 | 118 | if (firstPricingIndex == lastPricingIndex) { 119 | $.rewards[firstPricingIndex].addReward(restFee, finalizeLastChunk); 120 | } else { 121 | $.rewards[firstPricingIndex].addReward((feePerPricingChunk * firstPricingLength) / SECTORS_PER_PRICE, true); 122 | 123 | for (uint i = firstPricingIndex + 1; i < lastPricingIndex; i++) { 124 | $.rewards[i].addReward(feePerPricingChunk, true); 125 | } 126 | 127 | $.rewards[lastPricingIndex].addReward( 128 | (feePerPricingChunk * lastPricingLength) / SECTORS_PER_PRICE, 129 | finalizeLastChunk 130 | ); 131 | } 132 | } 133 | 134 | function claimMineReward(uint pricingIndex, address payable beneficiary, bytes32 minerID) external { 135 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 136 | require(_msgSender() == $.mine, "Sender does not have permission"); 137 | 138 | Reward memory reward = $.rewards[pricingIndex]; 139 | 140 | uint releasedReward = _releasedReward(reward); 141 | reward.updateReward(releasedReward); 142 | uint rewardAmount = reward.claimReward(); 143 | $.rewards[pricingIndex] = reward; 144 | 145 | uint approvedBaseReward = _baseReward(pricingIndex, reward, rewardAmount); 146 | uint actualBaseReward = $.totalBaseReward > approvedBaseReward ? approvedBaseReward : $.totalBaseReward; 147 | rewardAmount += actualBaseReward; 148 | $.totalBaseReward -= actualBaseReward; 149 | 150 | if (rewardAmount > 0) { 151 | _asyncTransfer(beneficiary, rewardAmount); 152 | emit DistributeReward(pricingIndex, beneficiary, minerID, rewardAmount); 153 | } 154 | } 155 | 156 | function setBaseReward(uint baseReward_) external onlyFoundationAdmin { 157 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 158 | $.baseReward = baseReward_; 159 | } 160 | 161 | function setServiceFeeRate(uint bps) external onlyFoundationAdmin { 162 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 163 | $.serviceFeeRateBps = bps; 164 | } 165 | 166 | function setFoundationAdmin(address foundationAdmin_) external onlyFoundationAdmin { 167 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 168 | $.foundationAdmin = foundationAdmin_; 169 | } 170 | 171 | function setTreasury(address treasury_) external onlyFoundationAdmin { 172 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 173 | $.treasury = treasury_; 174 | } 175 | 176 | function donate() external payable { 177 | ChunkRewardBaseStorage storage $ = _getChunkRewardBaseStorage(); 178 | $.totalBaseReward += msg.value; 179 | } 180 | 181 | function _releasedReward(Reward memory reward) internal view virtual returns (uint); 182 | 183 | function _baseReward( 184 | uint pricingIndex, 185 | Reward memory reward, 186 | uint rewardAmount 187 | ) internal view virtual returns (uint); 188 | } 189 | -------------------------------------------------------------------------------- /test/merkle.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Contract } from "ethers"; 3 | import { ethers } from "hardhat"; 4 | import { MerkleTreeTest } from "../typechain-types"; 5 | import { genLeaf, genLeaves, MockMerkle } from "./utils/mockMerkleTree"; 6 | 7 | function toBuffer(input: string): Buffer { 8 | return Buffer.from(input.slice(2), "hex"); 9 | } 10 | 11 | describe("Incremental merkle hash", function () { 12 | let merkle: MerkleTreeTest; 13 | 14 | beforeEach(async () => { 15 | const merkleABI = await ethers.getContractFactory("MerkleTreeTest"); 16 | merkle = await merkleABI.deploy(); 17 | }); 18 | 19 | it("init root", async () => { 20 | const ZEROS = Buffer.from(Array(32).fill(0)); 21 | const response = toBuffer(await merkle.root()); 22 | expect(response).to.deep.equal(ZEROS); 23 | }); 24 | 25 | it("one element", async () => { 26 | const allLeaves = await genLeaves(1); 27 | const tree = await new MockMerkle(allLeaves).build(); 28 | 29 | await merkle.insertNode(await genLeaf(1), 0); 30 | await merkle.commitRoot(); 31 | 32 | expect(await merkle.currentLength()).to.equal(2); 33 | expect(await merkle.unstagedHeight()).to.equal(2); 34 | const response = toBuffer(await merkle.root()); 35 | expect(response).to.deep.equal(tree.root()); 36 | }); 37 | 38 | it("padding element", async () => { 39 | const allLeaves: Buffer[] = await genLeaves(3); 40 | allLeaves[0] = await genLeaf(0); 41 | const tree = await new MockMerkle(allLeaves).build(); 42 | 43 | await merkle.insertNode(tree.at("1"), 1); 44 | await merkle.commitRoot(); 45 | 46 | expect(await merkle.currentLength()).to.equal(4); 47 | expect(await merkle.unstagedHeight()).to.equal(3); 48 | 49 | const response = toBuffer(await merkle.root()); 50 | expect(response).to.deep.equal(tree.root()); 51 | }); 52 | 53 | it("multiple insert with active commit", async () => { 54 | const allLeaves: Buffer[] = await genLeaves(7); 55 | allLeaves[0] = await genLeaf(0); 56 | 57 | const tree = await new MockMerkle(allLeaves).build(); 58 | 59 | await merkle.insertNode(tree.at("01"), 1); 60 | await merkle.commitRoot(); 61 | await merkle.insertNode(tree.at("10"), 1); 62 | await merkle.commitRoot(); 63 | await merkle.insertNode(tree.at("110"), 0); 64 | await merkle.commitRoot(); 65 | await merkle.insertNode(tree.at("111"), 0); 66 | await merkle.commitRoot(); 67 | 68 | expect(await merkle.currentLength()).to.equal(8); 69 | expect(await merkle.unstagedHeight()).to.equal(4); 70 | 71 | const response = toBuffer(await merkle.root()); 72 | expect(response).to.deep.equal(tree.root()); 73 | }); 74 | 75 | it("multiple insert with lazy commit", async () => { 76 | const allLeaves: Buffer[] = await genLeaves(7); 77 | allLeaves[0] = await genLeaf(0); 78 | 79 | const tree = await new MockMerkle(allLeaves).build(); 80 | 81 | await merkle.insertNode(tree.at("01"), 1); 82 | await merkle.insertNode(tree.at("10"), 1); 83 | await merkle.insertNode(tree.at("110"), 0); 84 | await merkle.insertNode(tree.at("111"), 0); 85 | await merkle.commitRoot(); 86 | 87 | expect(await merkle.currentLength()).to.equal(8); 88 | expect(await merkle.unstagedHeight()).to.equal(4); 89 | 90 | const response = toBuffer(await merkle.root()); 91 | expect(response).to.deep.equal(tree.root()); 92 | }); 93 | 94 | it("multiple insert with lazy commit and padding", async () => { 95 | const allLeaves: Buffer[] = await genLeaves(12); 96 | for (let i = 0; i < 3; i++) { 97 | allLeaves[i] = await genLeaf(0); 98 | } 99 | 100 | allLeaves[8] = await genLeaf(0); 101 | 102 | const tree = await new MockMerkle(allLeaves).build(); 103 | 104 | await merkle.insertNode(tree.at("01"), 2); 105 | await merkle.insertNode(tree.at("1000"), 0); 106 | await merkle.insertNode(tree.at("101"), 1); 107 | await merkle.insertNode(tree.at("1100"), 0); 108 | expect(await merkle.unstagedHeight()).to.equal(1); 109 | await merkle.commitRoot(); 110 | 111 | expect(await merkle.currentLength()).to.equal(13); 112 | expect(await merkle.unstagedHeight()).to.equal(5); 113 | 114 | const response = toBuffer(await merkle.root()); 115 | expect(response).to.deep.equal(tree.root()); 116 | }); 117 | 118 | it("merkle root consistency with random workload", async () => { 119 | const iterations = 5; 120 | const submissions = 25; 121 | const range = 12; 122 | for (let i = 0; i < iterations; i++) { 123 | const task = Array(submissions) 124 | .fill(0) 125 | .map(() => { 126 | return Math.floor(Math.random() * range); 127 | }); 128 | await testFromHeight(task); 129 | } 130 | }); 131 | 132 | it.skip("merkle root consistency with random workload (slow)", async () => { 133 | const iterations = 100; 134 | const submissions = 25; 135 | const range = 12; 136 | const tasks = []; 137 | for (let i = 0; i < iterations; i++) { 138 | const task = Array(submissions) 139 | .fill(0) 140 | .map(() => { 141 | return Math.floor(Math.random() * range); 142 | }); 143 | tasks.push(task); 144 | } 145 | await Promise.all(tasks.map((x) => testFromHeight(x))); 146 | }); 147 | }); 148 | 149 | async function buildLeafFromHeight(heights: number[]): Promise { 150 | const EMPTY_LEAF = await genLeaf(0); 151 | const leaves = []; 152 | while (heights.length > 0) { 153 | const height = heights[0]; 154 | if ((leaves.length + 1) % (1 << height) != 0) { 155 | leaves.push(EMPTY_LEAF); 156 | } else { 157 | for (let i = 0; i < 1 << height; i++) { 158 | leaves.push(await genLeaf(leaves.length + 1)); 159 | } 160 | heights = heights.slice(1); 161 | } 162 | } 163 | return await new MockMerkle(leaves).build(); 164 | } 165 | 166 | async function insertNodeFromHeight(merkle: Contract, heights: number[], tree: MockMerkle) { 167 | const totalHeight = tree.height(); 168 | let nextIndex = 1; 169 | 170 | const indexToPath = function (index: number, height: number) { 171 | let answer: string = ""; 172 | while (index > 0) { 173 | answer = (index % 2).toString() + answer; 174 | index = Math.floor(index / 2); 175 | } 176 | while (answer.length < totalHeight - 1) { 177 | answer = "0" + answer; 178 | } 179 | answer = answer.slice(0, answer.length - height); 180 | return answer; 181 | }; 182 | 183 | for (const height of heights) { 184 | nextIndex = Math.ceil(nextIndex / (1 << height)) * (1 << height); 185 | await merkle.insertNode(tree.at(indexToPath(nextIndex, height)), height); 186 | nextIndex += 1 << height; 187 | } 188 | } 189 | 190 | async function testFromHeight(heights: number[]) { 191 | const merkleABI = await ethers.getContractFactory("MerkleTreeTest"); 192 | const merkle = await merkleABI.deploy(); 193 | const tree = await buildLeafFromHeight(heights); 194 | await insertNodeFromHeight(merkle, heights, tree); 195 | await merkle.commitRoot(); 196 | 197 | const response = toBuffer(await merkle.root()); 198 | expect(response).to.deep.equal(tree.root()); 199 | } 200 | -------------------------------------------------------------------------------- /CONTRACT_PARAMETERS.md: -------------------------------------------------------------------------------- 1 | # Contract Parameters Documentation 2 | 3 | This document outlines all configurable pa3. **Runtime Configuration**: 4 | 5 | - [ ] Set appropriate service fee rates 6 | - [ ] Configure treasury addresses 7 | - [ ] Set mining parameters for launch 8 | - [ ] Verify all role assignments 9 | 10 | 4. **Parameter Relationships**: 11 | - [ ] Mining parameters are consistent 12 | - [ ] Reward economics are balancedhe 0G Storage smart contracts, including initialization parameters, runtime configuration options, and immutable deployment settings. 13 | 14 | --- 15 | 16 | ## Contract Parameters Overview 17 | 18 | ### 1. **PoraMine Contract** (`contracts/miner/Mine.sol`) 19 | 20 | #### **Constructor Parameters** 21 | 22 | | Parameter | Type | Description | Default Values | Changeable | 23 | | ---------- | ------ | ----------------------------- | ------------------------------------------------------------------------------ | ---------- | 24 | | `settings` | `uint` | Bit flags for mining features | Bitwise flags: NO_DATA_SEAL (0x1), NO_DATA_PROOF (0x2), FIXED_DIFFICULTY (0x4) | No | 25 | 26 | #### **Initialization Parameters** 27 | 28 | | Parameter | Type | Description | Default Value | Changeable | 29 | | ------------ | --------- | ------------------------- | -------------------------------------- | ---------- | 30 | | `difficulty` | `uint` | Initial mining difficulty | Used to calculate initial `poraTarget` | No | 31 | | `flow_` | `address` | Flow contract address | - | No | 32 | | `reward_` | `address` | Reward contract address | - | No | 33 | 34 | #### **Default Initialized Values** 35 | 36 | | Parameter | Type | Value | Description | Changeable | 37 | | ---------------------------- | -------- | ----- | -------------------------------------- | ---------- | 38 | | `targetMineBlocks` | `uint` | 100 | Target blocks for mining window | Yes | 39 | | `targetSubmissions` | `uint` | 10 | Target submissions per epoch | Yes | 40 | | `targetSubmissionsNextEpoch` | `uint` | 10 | Target submissions for admin to change and take effect on next epoch | Yes | 41 | | `difficultyAdjustRatio` | `uint` | 20 | Difficulty adjustment smoothing factor | Yes | 42 | | `maxShards` | `uint64` | 4 | Maximum shards for storage | Yes | 43 | | `nSubtasks` | `uint` | 1 | ? | Yes | 44 | 45 | targetMineBlocks = submission window = 10min -> n blocks 46 | 47 | nSubtasks needs to add logic to increase step time 48 | 49 | --- 50 | 51 | ### 2. **Flow Contract** (`contracts/dataFlow/Flow.sol`) 52 | 53 | #### **Constructor Parameters** 54 | 55 | | Parameter | Type | Description | Changeable | 56 | | -------------- | ------ | -------------------------------------------- | ---------- | 57 | | `deployDelay_` | `uint` | Delay before contract activation (immutable) | No | 58 | 59 | #### **Initialization Parameters** 60 | 61 | | Parameter | Type | Description | Changeable | 62 | | ----------------- | --------- | ----------------------- | ---------- | 63 | | `market_` | `address` | Market contract address | No | 64 | | `blocksPerEpoch_` | `uint` | Blocks per mining epoch | No | 65 | 66 | #### **Internal Parameters** (set once during initialization) 67 | 68 | | Parameter | Type | Description | Default | Changeable | 69 | | ------------- | --------- | ----------------------------------------- | ------------------------------- | ---------- | 70 | | `firstBlock` | `uint` | Block number when contract becomes active | `block.number + deployDelay` | No | 71 | | `rootHistory` | `address` | Digest history contract | Auto-created with 1000 capacity | No | 72 | 73 | #### **Constants** 74 | 75 | | Parameter | Type | Value | Description | Changeable | 76 | | ----------------------- | ------ | ----- | ------------------------- | ---------- | 77 | | `MAX_DEPTH` | `uint` | 64 | Maximum merkle tree depth | No | 78 | | `ROOT_AVAILABLE_WINDOW` | `uint` | 1000 | Root history capacity | No | 79 | 80 | --- 81 | 82 | ### 3. **ChunkRewardBase Contract** (`contracts/reward/ChunkRewardBase.sol`) 83 | 84 | #### **Initialization Parameters** 85 | 86 | | Parameter | Type | Description | Changeable | 87 | | --------- | --------- | ----------------------- | ---------- | 88 | | `market_` | `address` | Market contract address | No | 89 | | `mine_` | `address` | Mine contract address | No | 90 | 91 | #### **Runtime Configurable Parameters** (PARAMS_ADMIN_ROLE) 92 | 93 | | Function | Parameter | Type | Description | Changeable | 94 | | ------------------- | ------------- | --------- | ------------------------------------------ | ---------- | 95 | | `setBaseReward` | `baseReward_` | `uint` | Base reward amount per mining claim | Yes | 96 | | `setServiceFeeRate` | `bps` | `uint` | Service fee rate in basis points (0-10000) | Yes | 97 | | `setTreasury` | `treasury_` | `address` | Treasury address for service fees | Yes | 98 | 99 | --- 100 | 101 | ### 4. **ChunkDecayReward Contract** (`contracts/reward/ChunkDecayReward.sol`) 102 | 103 | #### **Constructor Parameters** 104 | 105 | | Parameter | Type | Description | Changeable | 106 | | ----------------------- | -------- | -------------------------------------------- | ---------- | 107 | | `annualMilliDecayRate_` | `uint16` | Annual decay rate in milli-units (immutable) | No | 108 | 109 | _Inherits all parameters from ChunkRewardBase_ 110 | 111 | --- 112 | 113 | ### 5. **ChunkLinearReward Contract** (`contracts/reward/ChunkLinearReward.sol`) 114 | 115 | #### **Constructor Parameters** 116 | 117 | | Parameter | Type | Description | Changeable | 118 | | ----------------- | ------ | ---------------------------------------------- | ---------- | 119 | | `releaseSeconds_` | `uint` | Linear release duration in seconds (immutable) | No | 120 | 121 | _Inherits all parameters from ChunkRewardBase_ 122 | 123 | --- 124 | 125 | ### 6. **OnePoolReward Contract** (`contracts/reward/OnePoolReward.sol`) 126 | 127 | #### **Constructor Parameters** 128 | 129 | | Parameter | Type | Description | Changeable | 130 | | ------------------ | ------ | ----------------------------------------------- | ---------- | 131 | | `lifetimeSeconds_` | `uint` | Lifetime for reward pool in seconds (immutable) | No | 132 | 133 | #### **Initialization Parameters** 134 | 135 | | Parameter | Type | Description | Changeable | 136 | | --------- | --------- | ----------------------- | ---------- | 137 | | `market_` | `address` | Market contract address | No | 138 | | `mine_` | `address` | Mine contract address | No | 139 | 140 | --- 141 | 142 | ### 7. **DigestHistory Contract** (`contracts/utils/DigestHistory.sol`) 143 | 144 | #### **Constructor Parameters** 145 | 146 | | Parameter | Type | Description | Changeable | 147 | | ---------- | ------ | ---------------------------------------------- | ---------- | 148 | | `capacity` | `uint` | Maximum number of digests to store (immutable) | No | 149 | -------------------------------------------------------------------------------- /contracts/miner/MineLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | 4 | import "../utils/ZgsSpec.sol"; 5 | import "../utils/Blake2b.sol"; 6 | import "../utils/DeepCopy.sol"; 7 | import "./RecallRange.sol"; 8 | 9 | library MineLib { 10 | using DeepCopy for bytes32[2]; 11 | 12 | struct PoraAnswer { 13 | bytes32 contextDigest; 14 | bytes32 nonce; 15 | bytes32 minerId; 16 | RecallRange range; 17 | uint recallPosition; 18 | uint sealOffset; 19 | bytes32 sealedContextDigest; 20 | bytes32[UNITS_PER_SEAL] sealedData; 21 | bytes32[] merkleProof; 22 | } 23 | 24 | function scratchPadHash(bytes32[2] memory padDigest, uint rounds) internal pure { 25 | assembly { 26 | for { 27 | let i := 0 28 | } lt(i, rounds) { 29 | i := add(i, 1) 30 | } { 31 | mstore(padDigest, keccak256(padDigest, 0x40)) 32 | mstore(add(padDigest, 0x20), keccak256(padDigest, 0x40)) 33 | } 34 | } 35 | } 36 | 37 | function scratchPadHashOnce(bytes32[2] memory padDigest) internal pure { 38 | assembly { 39 | mstore(padDigest, keccak256(padDigest, 0x40)) 40 | mstore(add(padDigest, 0x20), keccak256(padDigest, 0x40)) 41 | } 42 | } 43 | 44 | function computeScratchPadAndMix( 45 | bytes32[UNITS_PER_SEAL] memory sealedData, 46 | uint skipSeals, 47 | bytes32[2] memory padDigest 48 | ) internal pure returns (bytes32[2] memory recallSeed, bytes32[UNITS_PER_SEAL] memory mixedData) { 49 | bytes32[2] memory currentDigest = padDigest.deepCopy(); 50 | 51 | scratchPadHash(currentDigest, skipSeals * BHASHES_PER_SEAL); 52 | unchecked { 53 | for (uint i = 0; i < UNITS_PER_SEAL; i += 2) { 54 | scratchPadHashOnce(currentDigest); 55 | 56 | mixedData[i] = currentDigest[0] ^ sealedData[i]; 57 | mixedData[i + 1] = currentDigest[1] ^ sealedData[i + 1]; 58 | } 59 | } 60 | scratchPadHash(currentDigest, (SEALS_PER_PAD - skipSeals - 1) * BHASHES_PER_SEAL); 61 | 62 | recallSeed = currentDigest.deepCopy(); 63 | } 64 | 65 | function computePoraHash( 66 | uint sealOffset, 67 | bytes32[2] memory padSeed, 68 | bytes32[UNITS_PER_SEAL] memory mixedData 69 | ) internal view returns (bytes32) { 70 | bytes32[2] memory h; 71 | h[0] = Blake2b.BLAKE2B_INIT_STATE0; 72 | h[1] = Blake2b.BLAKE2B_INIT_STATE1; 73 | h = Blake2b.blake2bF(h, bytes32(sealOffset), padSeed[0], padSeed[1], bytes32(0), 128, false); 74 | 75 | // Update the blake2b hasher with the input `mixedData` and compute the blake2b hash. 76 | // 77 | // EVM is optimized for 32-byte aligned memory accesses, but the bultin contract 78 | // blake2b's parameters aren't always aligned. We allocate memory to align most parameters to 79 | // 32 bytes and use assembly to set up the blake2b parameters. 80 | // The memory space for parameters is reused across calls to save gas. 81 | bytes32[8] memory slots; 82 | uint offset = 128; 83 | uint finalizeOffset = 128 + UNITS_PER_SEAL * 32; 84 | bool blake2bError = false; 85 | 86 | assembly { 87 | let argPtr := add(slots, 0x1c) 88 | let roundPtr := add(slots, 0x1f) 89 | let hPtr := add(slots, 0x20) 90 | let mPtr := add(slots, 0x60) 91 | let offsetLo := add(slots, 0xe0) 92 | let offsetHi := add(slots, 0xe1) 93 | let finalizePtr := add(slots, 0xf0) 94 | 95 | let dataPtr := mixedData 96 | 97 | mstore8(roundPtr, 12) 98 | 99 | mstore(hPtr, mload(h)) 100 | mstore(add(hPtr, 0x20), mload(add(h, 0x20))) 101 | 102 | for { 103 | 104 | } and(lt(offset, finalizeOffset), not(blake2bError)) { 105 | 106 | } { 107 | offset := add(offset, 0x80) 108 | mstore8(offsetLo, and(offset, 0xff)) 109 | mstore8(offsetHi, shr(8, offset)) 110 | 111 | mstore(mPtr, mload(dataPtr)) 112 | mstore(add(mPtr, 0x20), mload(add(dataPtr, 0x20))) 113 | mstore(add(mPtr, 0x40), mload(add(dataPtr, 0x40))) 114 | mstore(add(mPtr, 0x60), mload(add(dataPtr, 0x60))) 115 | dataPtr := add(dataPtr, 128) 116 | 117 | if eq(offset, finalizeOffset) { 118 | mstore8(finalizePtr, 1) 119 | } 120 | 121 | if iszero(staticcall(not(0), 0x09, argPtr, 0xd5, hPtr, 0x40)) { 122 | blake2bError := true 123 | } 124 | } 125 | } 126 | require(!blake2bError, "blake2b internal error at PoRA hash"); 127 | // The blake2b hash locates at slots[1] and slots[2]. 128 | // Here we only return the first 32 bytes of the blake2b hash. 129 | return slots[1]; 130 | } 131 | 132 | function unseal(PoraAnswer memory answer) internal pure returns (bytes32[UNITS_PER_SEAL] memory unsealedData) { 133 | unsealedData[0] = 134 | answer.sealedData[0] ^ 135 | keccak256(abi.encode(answer.minerId, answer.sealedContextDigest, answer.recallPosition)); 136 | bytes32[UNITS_PER_SEAL] memory sealedData = answer.sealedData; 137 | 138 | // Equivalent to 139 | // unsealedData[i] = answer.sealedData[i] ^ keccak256(abi.encode(answer.sealedData[i - 1])); 140 | uint length = UNITS_PER_SEAL; 141 | assembly { 142 | let sealedPtr := sealedData 143 | let unsealedPtr := unsealedData 144 | 145 | let lastUnsealedPtr := add(unsealedPtr, mul(sub(length, 1), 0x20)) 146 | 147 | for { 148 | 149 | } lt(unsealedPtr, lastUnsealedPtr) { 150 | 151 | } { 152 | let mask := keccak256(sealedPtr, 0x20) 153 | 154 | sealedPtr := add(sealedPtr, 0x20) 155 | let data := mload(sealedPtr) 156 | 157 | unsealedPtr := add(unsealedPtr, 0x20) 158 | mstore(unsealedPtr, xor(data, mask)) 159 | } 160 | } 161 | } 162 | 163 | function recoverMerkleRoot( 164 | PoraAnswer memory answer, 165 | bytes32[UNITS_PER_SEAL] memory unsealedData 166 | ) internal pure returns (bytes32) { 167 | // Compute leaf of hash 168 | for (uint i = 0; i < UNITS_PER_SEAL; i += UNITS_PER_SECTOR) { 169 | bytes32 x; 170 | assembly { 171 | x := keccak256(add(unsealedData, mul(i, 32)), 256 /*BYTES_PER_SECTOR*/) 172 | } 173 | unsealedData[i] = x; 174 | } 175 | 176 | for (uint i = UNITS_PER_SECTOR; i < UNITS_PER_SEAL; i <<= 1) { 177 | for (uint j = 0; j < UNITS_PER_SEAL; j += i << 1) { 178 | bytes32 left = unsealedData[j]; 179 | bytes32 right = unsealedData[j + i]; 180 | unsealedData[j] = keccak256(abi.encode(left, right)); 181 | } 182 | } 183 | bytes32 currentHash = unsealedData[0]; 184 | delete unsealedData; 185 | 186 | uint unsealedIndex = answer.recallPosition / SECTORS_PER_SEAL; 187 | 188 | for (uint i = 0; i < answer.merkleProof.length; i += 1) { 189 | bytes32 left; 190 | bytes32 right; 191 | if (unsealedIndex % 2 == 0) { 192 | left = currentHash; 193 | right = answer.merkleProof[i]; 194 | } else { 195 | left = answer.merkleProof[i]; 196 | right = currentHash; 197 | } 198 | currentHash = keccak256(abi.encode(left, right)); 199 | 200 | unsealedIndex /= 2; 201 | } 202 | return currentHash; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /contracts/utils/Exponent.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0 <0.9.0; 3 | import "./BitMask.sol"; 4 | import "./UncheckedMath.sol"; 5 | 6 | library Exponential { 7 | int public constant LOG2X128 = 235865763225513294137944142764154484399; 8 | int private constant LOG2_2X128 = 163489688770384171654468164494102986538; 9 | int private constant LOG2_3X128 = 113322416821814740463990287346838243787; 10 | int private constant LOG2_4X128 = 78549113714279805586246364391047974677; 11 | 12 | int public constant INV_LOG2X128 = 490923683258796565746369346286093237521; 13 | 14 | uint private constant ROOT_POW10X127 = 0x8016302f174676283690dfe44d11d008; 15 | uint private constant ROOT_POW9X127 = 0x802c6436d0e04f50ff8ce94a6797b3ce; 16 | uint private constant ROOT_POW8X127 = 0x8058d7d2d5e5f6b094d589f608ee4aa2; 17 | uint private constant ROOT_POW7X127 = 0x80b1ed4fd999ab6c25335719b6e6fd20; 18 | uint private constant ROOT_POW6X127 = 0x8164d1f3bc0307737be56527bd14def4; 19 | uint private constant ROOT_POW5X127 = 0x82cd8698ac2ba1d73e2a475b46520bff; 20 | uint private constant ROOT_POW4X127 = 0x85aac367cc487b14c5c95b8c2154c1b2; 21 | uint private constant ROOT_POW3X127 = 0x8b95c1e3ea8bd6e6fbe4628758a53c90; 22 | uint private constant ROOT_POW2X127 = 0x9837f0518db8a96f46ad23182e42f6f6; 23 | uint private constant ROOT_POW1X127 = 0xb504f333f9de6484597d89b3754abe9f; 24 | 25 | uint private constant REV_ROOT_POW10X128 = 0xffd3a751c0f7e10bd3b9f8ae012fbe06; 26 | uint private constant REV_ROOT_POW9X128 = 0xffa756521c8daed19f3a1b48fb94c589; 27 | uint private constant REV_ROOT_POW8X128 = 0xff4ecb59511ec8a5301ba217ef18dd7c; 28 | uint private constant REV_ROOT_POW7X128 = 0xfe9e115c7b8f884badd25995e79d2f09; 29 | uint private constant REV_ROOT_POW6X128 = 0xfd3e0c0cf486c174853f3a5931e0ee03; 30 | uint private constant REV_ROOT_POW5X128 = 0xfa83b2db722a033a7c25bb14315d7fcc; 31 | uint private constant REV_ROOT_POW4X128 = 0xf5257d152486cc2c7b9d0c7aed980fc3; 32 | uint private constant REV_ROOT_POW3X128 = 0xeac0c6e7dd24392ed02d75b3706e54fa; 33 | uint private constant REV_ROOT_POW2X128 = 0xd744fccad69d6af439a68bb9902d3fde; 34 | uint private constant REV_ROOT_POW1X128 = 0xb504f333f9de6484597d89b3754abe9f; 35 | 36 | function powTwo64X96(uint exponentX64) internal pure returns (uint powerX96) { 37 | uint fractionSmallX64 = exponentX64 & 0x3fffffffffffff; 38 | uint fractionLargeX64 = exponentX64 & 0xffc0000000000000; 39 | uint integer = exponentX64 >> 64; 40 | if (integer >= 160) { 41 | return type(uint).max; 42 | } 43 | 44 | uint powerX127 = _talorExpand64X127(int(fractionSmallX64)); 45 | if ((fractionLargeX64 & BIT54) != 0) { 46 | unchecked { 47 | powerX127 *= ROOT_POW10X127; 48 | } 49 | powerX127 >>= 127; 50 | } 51 | if ((fractionLargeX64 & BIT55) != 0) { 52 | unchecked { 53 | powerX127 *= ROOT_POW9X127; 54 | } 55 | powerX127 >>= 127; 56 | } 57 | if ((fractionLargeX64 & BIT56) != 0) { 58 | unchecked { 59 | powerX127 *= ROOT_POW8X127; 60 | } 61 | powerX127 >>= 127; 62 | } 63 | if ((fractionLargeX64 & BIT57) != 0) { 64 | unchecked { 65 | powerX127 *= ROOT_POW7X127; 66 | } 67 | powerX127 >>= 127; 68 | } 69 | if ((fractionLargeX64 & BIT58) != 0) { 70 | unchecked { 71 | powerX127 *= ROOT_POW6X127; 72 | } 73 | powerX127 >>= 127; 74 | } 75 | if ((fractionLargeX64 & BIT59) != 0) { 76 | unchecked { 77 | powerX127 *= ROOT_POW5X127; 78 | } 79 | powerX127 >>= 127; 80 | } 81 | if ((fractionLargeX64 & BIT60) != 0) { 82 | unchecked { 83 | powerX127 *= ROOT_POW4X127; 84 | } 85 | powerX127 >>= 127; 86 | } 87 | if ((fractionLargeX64 & BIT61) != 0) { 88 | unchecked { 89 | powerX127 *= ROOT_POW3X127; 90 | } 91 | powerX127 >>= 127; 92 | } 93 | if ((fractionLargeX64 & BIT62) != 0) { 94 | unchecked { 95 | powerX127 *= ROOT_POW2X127; 96 | } 97 | powerX127 >>= 127; 98 | } 99 | if ((fractionLargeX64 & BIT63) != 0) { 100 | unchecked { 101 | powerX127 *= ROOT_POW1X127; 102 | } 103 | powerX127 >>= 127; 104 | } 105 | 106 | if (integer < 31) { 107 | powerX96 = powerX127 >> (31 - integer); 108 | } else { 109 | powerX96 = powerX127 << (integer - 31); 110 | } 111 | } 112 | 113 | function powHalf64X96(uint exponentX64) internal pure returns (uint powerX96) { 114 | uint fractionSmallX64 = exponentX64 & 0x3fffffffffffff; 115 | uint fractionLargeX64 = exponentX64 & 0xffc0000000000000; 116 | uint integer = exponentX64 >> 64; 117 | 118 | uint powerX127 = _talorExpand64X127(-int(fractionSmallX64)); 119 | if ((fractionLargeX64 & BIT54) != 0) { 120 | unchecked { 121 | powerX127 *= REV_ROOT_POW10X128; 122 | } 123 | powerX127 >>= 128; 124 | } 125 | if ((fractionLargeX64 & BIT55) != 0) { 126 | unchecked { 127 | powerX127 *= REV_ROOT_POW9X128; 128 | } 129 | powerX127 >>= 128; 130 | } 131 | if ((fractionLargeX64 & BIT56) != 0) { 132 | unchecked { 133 | powerX127 *= REV_ROOT_POW8X128; 134 | } 135 | powerX127 >>= 128; 136 | } 137 | if ((fractionLargeX64 & BIT57) != 0) { 138 | unchecked { 139 | powerX127 *= REV_ROOT_POW7X128; 140 | } 141 | powerX127 >>= 128; 142 | } 143 | if ((fractionLargeX64 & BIT58) != 0) { 144 | unchecked { 145 | powerX127 *= REV_ROOT_POW6X128; 146 | } 147 | powerX127 >>= 128; 148 | } 149 | if ((fractionLargeX64 & BIT59) != 0) { 150 | unchecked { 151 | powerX127 *= REV_ROOT_POW5X128; 152 | } 153 | powerX127 >>= 128; 154 | } 155 | if ((fractionLargeX64 & BIT60) != 0) { 156 | unchecked { 157 | powerX127 *= REV_ROOT_POW4X128; 158 | } 159 | powerX127 >>= 128; 160 | } 161 | if ((fractionLargeX64 & BIT61) != 0) { 162 | unchecked { 163 | powerX127 *= REV_ROOT_POW3X128; 164 | } 165 | powerX127 >>= 128; 166 | } 167 | if ((fractionLargeX64 & BIT62) != 0) { 168 | unchecked { 169 | powerX127 *= REV_ROOT_POW2X128; 170 | } 171 | powerX127 >>= 128; 172 | } 173 | if ((fractionLargeX64 & BIT63) != 0) { 174 | unchecked { 175 | powerX127 *= REV_ROOT_POW1X128; 176 | } 177 | powerX127 >>= 128; 178 | } 179 | 180 | powerX96 = powerX127 >> (31 + integer); 181 | } 182 | 183 | function _talorExpand64X127(int exponentX64) internal pure returns (uint powerX127) { 184 | unchecked { 185 | if (exponentX64 == 0) { 186 | return 1 << 127; 187 | } 188 | int x = exponentX64 << 63; 189 | int x2 = (x * x) >> 127; 190 | int x3 = (x2 * x) >> 127; 191 | int x4 = (x3 * x) >> 127; 192 | 193 | int powerX128 = 0; 194 | // 2^x = e^(ln(2) * x) = 1 + ln(2) * x + ln(2)^2 * x^2 / 2 + ln(2)^3 * x^3 / 6 + ln(2)^4 * x^4 / 24 195 | powerX128 += 1 << 128; // 1 196 | powerX128 += (x * LOG2X128) >> 127; // x 197 | powerX128 += (x2 * LOG2_2X128) >> 128; // x^2 / 2 198 | powerX128 += ((x3 * LOG2_3X128) >> 127) / 6; // x^3 / 6 199 | powerX128 += ((x4 * LOG2_4X128) >> 127) / 24; // x^4 / 24 200 | 201 | powerX127 = uint(powerX128) >> 1; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /scripts/flow-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Flow Context Update Manager 4 | # Single script to handle daily flow context updates with tmux daemon support 5 | 6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" 8 | LOG_DIR="$PROJECT_DIR/logs" 9 | PID_FILE="$LOG_DIR/flow-daemon.pid" 10 | SCRIPT_PATH="$SCRIPT_DIR/$(basename "$0")" 11 | NETWORK="${FLOW_UPDATE_NETWORK:-zgTestnetTurbo}" 12 | 13 | # Create logs directory 14 | mkdir -p "$LOG_DIR" 15 | 16 | # Logging function 17 | log() { 18 | local log_file="$LOG_DIR/flow-update-$(date +%Y%m%d).log" 19 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$log_file" 20 | } 21 | 22 | # Check if daemon is running 23 | daemon_running() { 24 | [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null 25 | } 26 | 27 | # Run single update 28 | run_update() { 29 | log "Starting flow context update on network: $NETWORK" 30 | 31 | if [[ -z "$DEPLOYER_KEY" ]]; then 32 | log "WARNING: DEPLOYER_KEY not set. Using default key for gas payments." 33 | fi 34 | 35 | cd "$PROJECT_DIR" 36 | local log_file="$LOG_DIR/flow-update-$(date +%Y%m%d).log" 37 | 38 | if npx hardhat flow:updatecontext --network "$NETWORK" >> "$log_file" 2>&1; then 39 | log "Flow context update completed successfully" 40 | return 0 41 | else 42 | log "ERROR: Flow context update failed" 43 | return 1 44 | fi 45 | } 46 | 47 | # Check if should run today 48 | should_run_today() { 49 | local today=$(date +%Y%m%d) 50 | local last_run_file="$LOG_DIR/.last_run" 51 | 52 | if [[ -f "$last_run_file" ]] && [[ "$(cat "$last_run_file")" == "$today" ]]; then 53 | return 1 54 | fi 55 | 56 | echo "$today" > "$last_run_file" 57 | return 0 58 | } 59 | 60 | # Daemon loop function 61 | daemon_loop() { 62 | local log_file="$LOG_DIR/flow-update-$(date +%Y%m%d).log" 63 | 64 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting flow update daemon..." | tee -a "$log_file" 65 | 66 | # Run initial update immediately on startup 67 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Running initial flow update on startup..." | tee -a "$log_file" 68 | if run_update; then 69 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Initial flow update completed successfully" | tee -a "$log_file" 70 | else 71 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Initial flow update failed" | tee -a "$log_file" 72 | fi 73 | 74 | while true; do 75 | # Get current time 76 | current_hour=$(date +%H) 77 | current_minute=$(date +%M) 78 | 79 | # Check if it's 2 AM (02:00) 80 | if [ "$current_hour" = "02" ] && [ "$current_minute" = "00" ]; then 81 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Running daily flow update..." | tee -a "$log_file" 82 | 83 | # Run the update using the same function 84 | if run_update; then 85 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Daily flow update completed successfully" | tee -a "$log_file" 86 | else 87 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] Daily flow update failed" | tee -a "$log_file" 88 | fi 89 | 90 | # Sleep for 60 seconds to avoid running multiple times in the same minute 91 | sleep 60 92 | else 93 | # Sleep for 30 seconds before checking again 94 | sleep 30 95 | fi 96 | done 97 | } 98 | 99 | # Start daemon 100 | start_daemon() { 101 | if daemon_running; then 102 | echo "Daemon already running (PID: $(cat "$PID_FILE"))" 103 | return 1 104 | fi 105 | 106 | local daemon_log="$LOG_DIR/daemon-$(date +%Y%m%d-%H%M%S).log" 107 | 108 | echo "Starting flow update daemon..." 109 | echo "Working directory: $PROJECT_DIR" 110 | echo "Network: $NETWORK" 111 | echo "Log file: $daemon_log" 112 | echo "Private Key: ${DEPLOYER_KEY:+✓ Set}${DEPLOYER_KEY:-⚠ Not set (using default)}" 113 | 114 | cd "$PROJECT_DIR" 115 | nohup bash -c "FLOW_UPDATE_NETWORK='$NETWORK' DEPLOYER_KEY='$DEPLOYER_KEY' '$SCRIPT_PATH' _daemon_loop" > "$daemon_log" 2>&1 & 116 | echo $! > "$PID_FILE" 117 | 118 | sleep 2 119 | if daemon_running; then 120 | echo "✓ Daemon started successfully (PID: $(cat "$PID_FILE"))" 121 | echo "Use '$0 logs' to view logs or '$0 stop' to stop" 122 | return 0 123 | else 124 | echo "ERROR: Daemon failed to start. Check log: $daemon_log" 125 | rm -f "$PID_FILE" 126 | return 1 127 | fi 128 | } 129 | 130 | # Stop daemon 131 | stop_daemon() { 132 | if ! daemon_running; then 133 | echo "Daemon not running" 134 | return 1 135 | fi 136 | 137 | local pid=$(cat "$PID_FILE") 138 | kill "$pid" 139 | echo "Daemon stopped (PID: $pid)" 140 | rm -f "$PID_FILE" 141 | } 142 | 143 | # Show status 144 | show_status() { 145 | echo "Flow Update Daemon Status:" 146 | echo " Network: $NETWORK" 147 | echo " Private Key: ${DEPLOYER_KEY:+✓ Configured}${DEPLOYER_KEY:-⚠ Not set (using default)}" 148 | 149 | if daemon_running; then 150 | echo " Status: ✓ Running (PID: $(cat "$PID_FILE"))" 151 | else 152 | echo " Status: ✗ Not running" 153 | fi 154 | 155 | # Show last run 156 | local last_run_file="$LOG_DIR/.last_run" 157 | if [[ -f "$last_run_file" ]]; then 158 | local last_run=$(cat "$last_run_file") 159 | echo " Last run: $last_run" 160 | fi 161 | } 162 | 163 | # View logs 164 | view_logs() { 165 | local latest_log=$(ls -t "$LOG_DIR"/flow-update-*.log 2>/dev/null | head -1) 166 | if [[ -n "$latest_log" ]]; then 167 | echo "Viewing: $latest_log" 168 | tail -f "$latest_log" 169 | else 170 | echo "No log files found" 171 | fi 172 | } 173 | 174 | # Main script 175 | case "${1:-help}" in 176 | start) 177 | start_daemon 178 | ;; 179 | stop) 180 | stop_daemon 181 | ;; 182 | restart) 183 | stop_daemon 184 | sleep 2 185 | start_daemon 186 | ;; 187 | status) 188 | show_status 189 | ;; 190 | logs) 191 | view_logs 192 | ;; 193 | run) 194 | run_update 195 | ;; 196 | debug) 197 | echo "=== Debug Information ===" 198 | echo "Script: $0" 199 | echo "Script Path: $SCRIPT_PATH" 200 | echo "Project Dir: $PROJECT_DIR" 201 | echo "Network: $NETWORK" 202 | echo "PID File: $PID_FILE" 203 | echo "Daemon running: $(daemon_running && echo "Yes (PID: $(cat "$PID_FILE"))" || echo "No")" 204 | echo "Environment:" 205 | echo " DEPLOYER_KEY: ${DEPLOYER_KEY:+Set}${DEPLOYER_KEY:-Not set}" 206 | echo " FLOW_UPDATE_NETWORK: ${FLOW_UPDATE_NETWORK:-Not set (using default)}" 207 | echo "" 208 | echo "=== Testing daemon loop (will run for 10 seconds) ===" 209 | timeout 10 "$SCRIPT_PATH" _daemon_loop || echo "Daemon loop test completed/failed" 210 | ;; 211 | test-daemon) 212 | echo "Testing daemon loop directly (Ctrl+C to stop)..." 213 | daemon_loop 214 | ;; 215 | _daemon_loop) 216 | daemon_loop 217 | ;; 218 | help|--help|-h) 219 | echo "Usage: $0 {start|stop|restart|status|logs|run|debug|test-daemon}" 220 | echo "" 221 | echo "Commands:" 222 | echo " start - Start daemon (runs daily at 2 AM)" 223 | echo " stop - Stop daemon" 224 | echo " restart - Restart daemon" 225 | echo " status - Show status" 226 | echo " logs - View logs" 227 | echo " run - Run update once manually" 228 | echo " debug - Show debug information and test daemon" 229 | echo " test-daemon - Test daemon loop directly (for debugging)" 230 | echo "" 231 | echo "Environment variables:" 232 | echo " DEPLOYER_KEY - Private key for gas payments (required)" 233 | echo " FLOW_UPDATE_NETWORK - Network (default: zgTestnetTurbo)" 234 | ;; 235 | *) 236 | echo "Unknown command: $1" 237 | echo "Use '$0 help' for usage" 238 | exit 1 239 | ;; 240 | esac 241 | -------------------------------------------------------------------------------- /ROLES_AND_PERMISSIONS.md: -------------------------------------------------------------------------------- 1 | # Roles and Money Control Permissions Documentation 2 | 3 | This document outlines all key roles, permissions, and financial control mechanisms across the 0G Storage smart contracts. 4 | 5 | ## Overview 6 | 7 | The 0G Storage system consists of several interconnected contracts that handle data storage, mining rewards, token economics, and fee management. Financial controls are distributed across multiple contracts with different role-based access patterns. 8 | 9 | --- 10 | 11 | ## Key Roles by Contract 12 | 13 | ### 1. **PoraMine Contract** (`contracts/miner/Mine.sol`) 14 | 15 | | Role | Contract | Permissions | Can Control Money? | Functions | 16 | | -------------------- | -------- | ----------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | 17 | | `DEFAULT_ADMIN_ROLE` | PoraMine | Full admin privileges, grant/revoke roles | Indirect | All admin functions | 18 | | `PARAMS_ADMIN_ROLE` | PoraMine | Mining parameter configuration | No | `setTargetMineBlocks`, `setTargetSubmissions`, `setDifficultyAdjustRatio`, `setMaxShards`, `setMinDifficulty`, `setNumSubtasks` | 19 | | Miner/Beneficiary | PoraMine | Receive mining rewards | Yes (receive only) | Rewards distributed via `submit()` | 20 | 21 | **Financial Impact**: No direct money withdrawal, but controls mining reward distribution through the reward contract. 22 | 23 | --- 24 | 25 | ### 2. **ChunkRewardBase Contract** (`contracts/reward/ChunkRewardBase.sol`) 26 | 27 | | Role | Contract | Permissions | Can Control Money? | Functions | 28 | | -------------------- | --------------- | --------------------------- | ------------------ | --------------------------------------------------- | 29 | | `DEFAULT_ADMIN_ROLE` | ChunkRewardBase | Full admin privileges | Indirect | Grant/revoke roles | 30 | | `PARAMS_ADMIN_ROLE` | ChunkRewardBase | Configure reward parameters | Yes (indirect) | `setBaseReward`, `setServiceFeeRate`, `setTreasury` | 31 | | `market` address | ChunkRewardBase | Deposit rewards | Yes | `fillReward` | 32 | | `mine` address | ChunkRewardBase | Distribute mining rewards | Yes | `claimMineReward` | 33 | | `treasury` address | ChunkRewardBase | Receive service fees | Yes (receive) | Automatically receives service fees | 34 | | Anyone | ChunkRewardBase | Donate to base reward pool | Yes | `donate` | 35 | 36 | **Financial Controls**: 37 | 38 | - Service fee collection (configurable rate) 39 | - Mining reward distribution 40 | - Treasury fee management 41 | - Base reward pool funding 42 | 43 | --- 44 | 45 | ### 3. **Flow Contract** (`contracts/dataFlow/Flow.sol`) 46 | 47 | | Role | Contract | Permissions | Can Control Money? | Functions | 48 | | -------------------- | -------- | ---------------------- | ------------------ | ------------------------- | 49 | | `DEFAULT_ADMIN_ROLE` | Flow | Full admin privileges | Indirect | Administrative functions | 50 | | `PAUSER_ROLE` | Flow | Pause/unpause contract | No | `pause`, `unpause` | 51 | | `market` address | Flow | Data flow management | No | Data submission functions | 52 | 53 | **Financial Impact**: Manages data flow but delegates financial operations to Reward contracts. 54 | 55 | --- 56 | 57 | ### 4. **StakeToken Contract** (`contracts/token/StakeToken.sol`) 58 | 59 | | Role | Contract | Permissions | Can Control Money? | Functions | 60 | | ----- | ---------- | ------------------------ | ------------------ | ------------------ | 61 | | Users | StakeToken | Stake/unstake ZGS tokens | Yes | `stake`, `unstake` | 62 | 63 | **Financial Controls**: 64 | 65 | - ZGS token staking 66 | - Proportional unstaking 67 | - No admin withdrawal functions 68 | 69 | --- 70 | 71 | ## Summary of Money Control Permissions 72 | 73 | ### **Direct Withdrawal/Transfer Capabilities** 74 | 75 | | Entity | Contract | Method | Description | 76 | | ------------- | --------------- | ------------------ | -------------------------------------- | 77 | | Treasury | ChunkRewardBase | Automatic transfer | Receives service fees | 78 | | Stake address | PeakSwap | Token transfers | Receives swap proceeds | 79 | | Miners | ChunkRewardBase | `_asyncTransfer()` | Mining reward distribution | 80 | | Users | StakeToken | `transfer()` | Stake/unstake operations | 81 | | Anyone | PeakSwap | `skim()` | Withdraw excess tokens beyond reserves | 82 | 83 | ### **Administrative Control Over Funds** 84 | 85 | | Role | Contract | Control Type | Impact | 86 | | ------------------- | --------------- | -------------------- | ----------------------------------------------- | 87 | | `PARAMS_ADMIN_ROLE` | ChunkRewardBase | Set treasury address | Controls where service fees go | 88 | | `PARAMS_ADMIN_ROLE` | ChunkRewardBase | Set service fee rate | Controls fee percentage | 89 | | `PARAMS_ADMIN_ROLE` | ChunkRewardBase | Set base reward | Controls additional mining rewards | 90 | | Contract deployer | All contracts | Initial setup | Sets critical addresses (stake, treasury, etc.) | 91 | 92 | ### **No Direct Admin Withdrawal** 93 | 94 | The following contracts **do not** have administrative withdrawal functions: 95 | 96 | - **PoraMine**: No admin can withdraw funds 97 | - **Flow**: No financial functions 98 | - **UploadToken**: No admin withdrawal capabilities 99 | - **StakeToken**: No admin withdrawal, only user stake/unstake 100 | 101 | --- 102 | 103 | ## Security Considerations 104 | 105 | ### **Centralization Risks** 106 | 107 | 1. **Treasury Control**: `PARAMS_ADMIN_ROLE` can change where service fees are sent 108 | 2. **Base Reward Control**: Admins can modify mining reward amounts 109 | 3. **Contract Pausing**: `PAUSER_ROLE` can halt operations 110 | 4. **Parameter Changes**: Various admin roles can modify economic parameters 111 | 112 | ### **Safeguards** 113 | 114 | 1. **No Direct Admin Withdrawal**: No role can directly withdraw user funds 115 | 2. **Automated Transfers**: Most fund movements are automatic based on protocol rules 116 | 3. **Role Separation**: Different aspects controlled by different roles 117 | 4. **Time-based Delays**: Some contracts may implement timelock controls 118 | 119 | ### **User Fund Safety** 120 | 121 | - Mining rewards are distributed through the mining contract, not admin-controlled 122 | - Staking tokens can be unstaked by users directly 123 | - No admin can directly access user balances 124 | 125 | --- 126 | 127 | ## Deployment Considerations 128 | 129 | When deploying, ensure: 130 | 131 | 1. Treasury addresses are set to appropriate multi-sig wallets 132 | 2. Admin roles are granted to governance contracts or multi-sig 133 | 3. Stake addresses are properly configured 134 | 4. Service fee rates are set to reasonable levels 135 | 5. All inter-contract addresses are correctly configured 136 | 137 | --- 138 | 139 | ## Parameters 140 | 141 | 1. blocksPerEpoch: Set epoch time to be 20 min, current block time is 400ms, then blocksPerEpoch = 3000 142 | 2. lifetimeMonth: 12 months by default 143 | 3. price: $11/TB/month 144 | 4. PricePerSector(256 Bytes)/Month: $\text{lifetimeMonth} * \text{unitPrice} * 256 * 1,000,000,000,000,000,000 / 1024 / 1024 / 1024 / 12$. 145 | 5. unitPrice: $\text{unitPrice} = \$11 / \text{pricePerToken} / 1024$ 146 | -------------------------------------------------------------------------------- /src/dev/view.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { Signer } from "ethers"; 4 | import { ethers } from "hardhat"; 5 | import { ChunkLinearReward, FixedPriceFlow, PoraMine } from "../../typechain-types"; 6 | import { TypedContractEvent, TypedEventLog } from "../../typechain-types/common"; 7 | 8 | import { NewEpochEvent } from "../../typechain-types/contracts/dataFlow/Flow"; 9 | import { NewSubmissionEvent } from "../../typechain-types/contracts/miner/Mine.sol/PoraMine"; 10 | import { DistributeRewardEvent } from "../../typechain-types/contracts/reward/ChunkLinearReward"; 11 | 12 | type TypedNewEpochEvent = TypedContractEvent< 13 | NewEpochEvent.InputTuple, 14 | NewEpochEvent.OutputTuple, 15 | NewEpochEvent.OutputObject 16 | >; 17 | type TypedDistributeRewardEvent = TypedContractEvent< 18 | DistributeRewardEvent.InputTuple, 19 | DistributeRewardEvent.OutputTuple, 20 | DistributeRewardEvent.OutputObject 21 | >; 22 | type TypedNewSubmissionEvent = TypedContractEvent< 23 | NewSubmissionEvent.InputTuple, 24 | NewSubmissionEvent.OutputTuple, 25 | NewSubmissionEvent.OutputObject 26 | >; 27 | const u256_max = 1n << 256n; 28 | 29 | interface ViewContracts { 30 | mine: PoraMine; 31 | flow: FixedPriceFlow; 32 | reward: ChunkLinearReward; 33 | } 34 | 35 | async function contracts(me: Signer): Promise { 36 | const mine = await ethers.getContractAt("PoraMine", "0x6815F41019255e00D6F34aAB8397a6Af5b6D806f", me); 37 | 38 | const flow = await ethers.getContractAt("FixedPriceFlow", "0xbD2C3F0E65eDF5582141C35969d66e34629cC768", me); 39 | 40 | const reward = await ethers.getContractAt("ChunkLinearReward", "0x51998C4d486F406a788B766d93510980ae1f9360", me); 41 | 42 | return { mine, flow, reward }; 43 | } 44 | 45 | async function getBlockNumber() { 46 | return (await ethers.provider.getBlock("latest")).number; 47 | } 48 | 49 | async function printStatus(flow: FixedPriceFlow, mine: PoraMine) { 50 | const latestBlock = await ethers.provider.getBlock("latest"); 51 | console.log("\n============= Blockchain information ============="); 52 | const currentBlock = latestBlock.number; 53 | const timestamp = latestBlock.timestamp; 54 | console.log("current block number: %d", currentBlock); 55 | console.log("current time: %s", new Date(timestamp * 1000).toString()); 56 | // console.log("gas price: %d", gasPrice); 57 | console.log("gas price: %s ", (await ethers.provider.getFeeData()).gasPrice); 58 | 59 | const context = await flow.makeContextWithResult.staticCall(); 60 | console.log("\n============= Flow information ============="); 61 | console.log("current length: %d (%d pricing chunks)", context.flowLength, context.flowLength / 33554432n); 62 | console.log("current flow root: %s", context.flowRoot); 63 | console.log("current context digest: %s", context.digest); 64 | 65 | const [firstBlock, blocksPerEpoch, epoch] = await Promise.all([ 66 | flow.firstBlock(), 67 | flow.blocksPerEpoch(), 68 | flow.epoch(), 69 | ]); 70 | console.log("\n============= Epoch information ============="); 71 | const nextEpoch = firstBlock + blocksPerEpoch * (epoch + 1n); 72 | console.log("first block: %d", firstBlock); 73 | console.log("blocks per epoch: %d", blocksPerEpoch); 74 | console.log("epoch: %d (expected %d)", epoch, context.epoch); 75 | console.log( 76 | "current epoch time: %d / %d", 77 | BigInt(currentBlock) - (firstBlock + blocksPerEpoch * epoch), 78 | blocksPerEpoch 79 | ); 80 | console.log("next epoch start: %d (%d blocks left)", nextEpoch, nextEpoch - BigInt(currentBlock)); 81 | 82 | const [ 83 | poraTarget, 84 | currentSubmissions, 85 | targetSubmissions, 86 | targetSubmissionsNextEpoch, 87 | targetMineBlocks, 88 | lastMinedEpoch, 89 | minimumQuality, 90 | canSubmit, 91 | ] = await Promise.all([ 92 | mine.poraTarget(), 93 | mine.currentSubmissions(), 94 | mine.targetSubmissions(), 95 | mine.targetSubmissionsNextEpoch(), 96 | mine.targetMineBlocks(), 97 | mine.lastMinedEpoch(), 98 | mine.minDifficulty(), 99 | mine.canSubmit.staticCall(), 100 | ]); 101 | console.log("\n============= Mine information ============="); 102 | console.log("target quality", u256_max / poraTarget); 103 | console.log("minimum quality", minimumQuality); 104 | console.log("current submissions: %d / %d", currentSubmissions, targetSubmissions); 105 | console.log("target submissions for next epoch: %d", targetSubmissionsNextEpoch); 106 | console.log("target mine blocks: %d", targetMineBlocks); 107 | console.log("last mined epoch: %d", lastMinedEpoch); 108 | console.log("can submit:", canSubmit); 109 | 110 | const [sealDataEnabled, dataProofEnabled, fixedDifficulty] = await Promise.all([ 111 | mine.sealDataEnabled(), 112 | mine.dataProofEnabled(), 113 | mine.fixedDifficulty(), 114 | ]); 115 | console.log("\n============= Mine config ============="); 116 | console.log("SealDataEnabled:", sealDataEnabled); 117 | console.log("DataProofEnabled:", sealDataEnabled); 118 | console.log("FixedDifficulty:", fixedDifficulty); 119 | 120 | console.log("<<<<<<<< Done <<<<<<<<<<\n"); 121 | } 122 | 123 | async function queryEvents( 124 | blocks: number, 125 | contract: BaseContract, 126 | filter: TypedDeferredTopicFilter, 127 | callback: (eventLog: TypedEventLog) => Promise 128 | ) { 129 | const GET_LOGS_RANGE = 1000; 130 | const n = await getBlockNumber(); 131 | let i = n; 132 | const startBlock = Math.max(0, n - blocks + 1); 133 | 134 | while (i > startBlock) { 135 | const queryEnd = i; 136 | const queryStart = Math.max(startBlock, i - GET_LOGS_RANGE + 1); 137 | const events = await contract.queryFilter(filter, i - GET_LOGS_RANGE + 1, i); 138 | for (const event of events.reverse()) { 139 | await callback(event); 140 | } 141 | i -= GET_LOGS_RANGE; 142 | } 143 | } 144 | 145 | async function printContext(flow: FixedPriceFlow, blocks: number = 1000) { 146 | console.log("====== New Epoch Events (last %d blocks) ======", blocks); 147 | 148 | const [firstBlock, blocksPerEpoch] = await Promise.all([flow.firstBlock(), flow.blocksPerEpoch()]); 149 | 150 | await queryEvents(blocks, flow, flow.filters.NewEpoch(), async function (event) { 151 | const args = event.args; 152 | const epoch = event.args.index; 153 | const epochStart = Number(firstBlock + blocksPerEpoch * epoch); 154 | 155 | const timestamp = 156 | (await ethers.provider.getBlock(epochStart))?.timestamp ?? 157 | (() => { 158 | throw new Error("Failed to fetch the block"); 159 | })(); 160 | const timeString = new Date(timestamp * 1000).toLocaleString(); 161 | 162 | console.log( 163 | "Epoch %d:\t Activated at %d (activate block) = %d (start block) + %d (block delay) \tcontext digest: %s\tsender: %s\tactivate time: %s\tflow length: %d", 164 | epoch, 165 | event.blockNumber, 166 | epochStart, 167 | event.blockNumber - epochStart, 168 | args.context, 169 | args.sender, 170 | timeString, 171 | args.flowLength 172 | ); 173 | }); 174 | console.log("<<<<<<<< Done <<<<<<<<<<\n"); 175 | } 176 | 177 | async function printReward(reward: ChunkLinearReward, blocks: number = 1000) { 178 | console.log("====== Reward Distribution Events (last %d blocks) ======", blocks); 179 | await queryEvents( 180 | blocks, 181 | reward, 182 | reward.filters.DistributeReward(), 183 | async function (event) { 184 | const [tx, receipt] = await Promise.all([event.getTransaction(), event.getTransactionReceipt()]); 185 | console.log( 186 | "Reward distributed at block %d\tGas: %d (%d)\ttx hash: %s", 187 | event.blockNumber, 188 | receipt.gasUsed, 189 | tx.gasLimit, 190 | event.transactionHash 191 | ); 192 | } 193 | ); 194 | console.log("<<<<<<<< Done <<<<<<<<<<\n"); 195 | } 196 | 197 | async function printMineSubmissions(mine: PoraMine, blocks: number = 1000) { 198 | console.log("====== Mine Submission Events (last %d blocks) ======", blocks); 199 | await queryEvents(blocks, mine, mine.filters.NewSubmission(), async function (event) { 200 | console.log( 201 | "Mine submission, epoch: %d\tindex: %d,\tposition: %d - %d\ttx hash: %s", 202 | event.args.epoch, 203 | event.args.epochIndex, 204 | event.args.recallPosition / 33554432n, 205 | event.args.recallPosition % 33554432n, 206 | event.transactionHash 207 | ); 208 | }); 209 | console.log("<<<<<<<< Done <<<<<<<<<<\n"); 210 | } 211 | 212 | async function printRewardPool(reward: ChunkLinearReward, chunks: number) { 213 | const toDate = function (timestamp) { 214 | const date = new Date(Number(timestamp * 1000n)); 215 | return date.toISOString(); 216 | }; 217 | const base = 1000000000000000n; 218 | console.log("====== Reward pool ======"); 219 | 220 | const [releaseSeconds, baseReward, totalBaseReward, firstRewardableChunk] = await Promise.all([ 221 | reward.releaseSeconds(), 222 | reward.baseReward(), 223 | reward.totalBaseReward(), 224 | reward.firstRewardableChunk(), 225 | ]); 226 | 227 | console.log("Note: 1000 mZG = 1 ZG"); 228 | console.log(`release days: ${releaseSeconds / 86400n}`); 229 | console.log(`base reward: ${baseReward / base} mZG`); 230 | console.log(`total base reward: ${totalBaseReward / base} mZG`); 231 | console.log(`first rewardable chunk: ${firstRewardableChunk}`); 232 | 233 | for (let i = firstRewardableChunk; i < chunks; i++) { 234 | const res = await reward.rewards(i); 235 | console.log( 236 | `[Pool ${i}]\tlocked: ${res.lockedReward / base} mZG,\tclaimable: ${ 237 | res.claimableReward / base 238 | } mZG,\tdistributed: ${res.distributedReward / base} mZG,\tstart time: ${toDate( 239 | res.startTime 240 | )},\tlast update: ${toDate(res.lastUpdate)}` 241 | ); 242 | } 243 | const res = await reward.rewards(chunks); 244 | console.log( 245 | `[Pool next]\treward: ${res.lockedReward / base},\tclaimable: ${res.claimableReward / base},\tdistributed: ${ 246 | res.distributedReward / base 247 | },\tstart time: ${toDate(res.startTime)},\tlast update: ${toDate(res.lastUpdate)}` 248 | ); 249 | console.log("<<<<<<<< Done <<<<<<<<<<\n"); 250 | } 251 | 252 | async function updateContext(flow: FixedPriceFlow) { 253 | const tx = await flow.makeContext(); 254 | const receipt = await tx.wait(); 255 | console.log(receipt); 256 | } 257 | 258 | async function main() { 259 | const [owner, me, me2] = await ethers.getSigners(); 260 | 261 | const { mine, flow, reward } = await contracts(me2); 262 | await printStatus(flow, mine); 263 | await printContext(flow); 264 | await printReward(reward); 265 | await printMineSubmissions(mine); 266 | const pricingChunks = Number((await flow.makeContextWithResult.staticCall()).flowLength / 33554432n); 267 | await printRewardPool(reward, pricingChunks); 268 | // await updateContext(flow); 269 | } 270 | 271 | main() 272 | .then(() => process.exit(0)) 273 | .catch((error) => { 274 | console.error(error); 275 | process.exit(1); 276 | }); 277 | --------------------------------------------------------------------------------