diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca4d6c4..cc468c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,83 +6,68 @@ on: paths: - 'contracts/**' - 'test/**' - - 'scripts/**' - - 'hardhat.config.ts' - - 'package.json' - - 'package-lock.json' + - 'script/**' + - 'foundry.toml' - '.github/workflows/ci.yml' pull_request: branches: ['*'] paths: - 'contracts/**' - 'test/**' - - 'scripts/**' - - 'hardhat.config.ts' - - 'package.json' - - 'package-lock.json' + - 'script/**' + - 'foundry.toml' - '.github/workflows/ci.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - NODE_VERSION: '24' - jobs: lint-and-format: name: Lint & Format runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - - run: npm ci --legacy-peer-deps + - name: Install dependencies + run: forge install --no-git foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts@v5.4.0 FhenixProtocol/cofhe-contracts FhenixProtocol/cofhe-mock-contracts OpenZeppelin/openzeppelin-foundry-upgrades - name: Check formatting - run: npm run format:check - - - name: Lint Solidity - run: npm run lint + run: forge fmt --check compile: name: Compile Contracts runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - - run: npm ci --legacy-peer-deps + - name: Install dependencies + run: forge install --no-git foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts@v5.4.0 FhenixProtocol/cofhe-contracts FhenixProtocol/cofhe-mock-contracts OpenZeppelin/openzeppelin-foundry-upgrades - name: Compile contracts - run: npm run compile + run: forge build --sizes test: name: Test Contracts runs-on: ubuntu-latest needs: compile timeout-minutes: 15 - env: - MOCHA_TIMEOUT: '120000' steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - - run: npm ci --legacy-peer-deps + - name: Install dependencies + run: forge install --no-git foundry-rs/forge-std OpenZeppelin/openzeppelin-contracts@v5.4.0 FhenixProtocol/cofhe-contracts FhenixProtocol/cofhe-mock-contracts OpenZeppelin/openzeppelin-foundry-upgrades - name: Run tests - run: npm test + run: forge test -vvv diff --git a/.gitignore b/.gitignore index bac400e..b30a2b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ cache/ +cache_forge/ artifacts/ typechain-types/ coverage/ @@ -7,3 +8,8 @@ coverage.json .env deployments/*.json !deployments/.gitkeep + +# Foundry +out/ +broadcast/ +lib/ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 2312dc5..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -npx lint-staged diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 40a55c4..0000000 --- a/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "overrides": [ - { - "files": "*.sol", - "options": { - "singleQuote": false - } - } - ], - "printWidth": 120, - "tabWidth": 2, - "trailingComma": "all", - "plugins": ["prettier-plugin-solidity"] -} diff --git a/.solhint.json b/.solhint.json deleted file mode 100644 index 505b4b0..0000000 --- a/.solhint.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "solhint:recommended", - "rules": { - "compiler-version": ["error", "^0.8.24"], - "func-visibility": ["warn", { "ignoreConstructors": true }], - "no-empty-blocks": "off", - "reason-string": "off", - "no-unused-vars": "warn" - } -} diff --git a/README.md b/README.md index 85a4f9b..8817d6a 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,66 @@ -# ReineiraOS Code +## Foundry -[![Platform](https://img.shields.io/badge/ReineiraOS-v0.1-blue)](https://reineira.xyz) -[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** -AI-assisted plugin development for ReineiraOS. Build condition resolvers and insurance policies with Claude Code. +Foundry consists of: -> **Platform 0.1** — Generates contracts compatible with ReineiraOS v0.1 interfaces. Check `reineira.json` for version details. +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. -## Setup +## Documentation -```bash -git clone https://github.com/ReineiraOS/reineira-code.git -cd reineira-code -npm install --legacy-peer-deps -cp .env.example .env -# Add your private key and RPC URL to .env -``` +https://book.getfoundry.sh/ ## Usage -Open in an editor with Claude Code. Use slash commands: - -| Command | What it does | -| ---------------- | ------------------------------------------------------ | -| `/new-resolver` | Build a condition resolver from a description | -| `/new-policy` | Build an insurance policy with FHE from a description | -| `/deploy` | Deploy any contract to Arbitrum Sepolia | -| `/test` | Run tests, diagnose and fix failures | -| `/audit` | Security audit against the protocol checklist | -| `/integrate` | Generate SDK code to attach your contract to an escrow | -| `/scaffold-test` | Generate tests for an existing contract | -| `/verify` | Verify a deployed contract on Arbiscan | - -### Example +### Build +```shell +$ forge build ``` -/new-resolver A resolver that verifies PayPal payment via zkTLS proof from Reclaim Protocol -``` - -Claude Code generates the Solidity contract, tests, and deployment script — all pre-configured for the ReineiraOS protocol. -## The ecosystem +### Test -| Repo | What you do there | Platform | -| ------------------------------------------------------------------ | ---------------------------------------------------------- | -------- | -| [reineira-atlas](https://github.com/ReineiraOS/reineira-atlas) | Run the startup — strategy, ops, growth, compliance, pitch | 0.1 | -| **reineira-code** (this repo) | Build smart contracts — resolvers, policies, tests, deploy | 0.1 | -| [platform-modules](https://github.com/ReineiraOS/platform-modules) | Ship the product — backend, platform app, payment link | 0.1 | +```shell +$ forge test +``` -All repos declare their platform compatibility in `reineira.json`. When the platform version bumps, breaking contract interface changes may require upgrading. +### Format -## Manual workflow +```shell +$ forge fmt +``` -```bash -# Compile -npm run compile +### Gas Snapshots -# Test -npm test +```shell +$ forge snapshot +``` -# Deploy -CONTRACT_NAME=MyResolver npm run deploy +### Anvil -# Verify on Arbiscan -npx hardhat verify --network arbitrumSepolia
+```shell +$ anvil ``` -## Compatibility +### Deploy -| Component | Requirement | -| --------- | ----------------------- | -| Platform | ReineiraOS 0.1 | -| Solidity | ^0.8.24 | -| Hardhat | ~2.26.x | -| SDK | @reineira-os/sdk ^0.1.0 | -| cofhejs | ^0.3.1 | -| Node.js | 18+ | +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` -## Documentation +### Cast -- [ReineiraOS Docs](https://reineira.xyz/docs) -- [Quick Start](https://reineira.xyz/docs/getting-started/quick-start) -- [Condition Plugins](https://reineira.xyz/docs/develop/condition-plugins) -- [Insurance Policies](https://reineira.xyz/docs/develop/insurance-policies) -- [Telegram](https://t.me/ReineiraOS) +```shell +$ cast +``` -## License +### Help -MIT +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/contracts/interfaces/IConditionResolver.sol b/contracts/interfaces/IConditionResolver.sol index c1f49b8..0b9371a 100644 --- a/contracts/interfaces/IConditionResolver.sol +++ b/contracts/interfaces/IConditionResolver.sol @@ -5,15 +5,15 @@ pragma solidity ^0.8.24; /// @notice Interface for escrow release condition plugins. /// @dev Implement this to control when a ConfidentialEscrow releases funds. interface IConditionResolver { - /// @notice Check if the release condition for an escrow is met. - /// @dev Called on every redeem attempt. MUST be a view function. - /// @param escrowId The sequential escrow identifier. - /// @return True if the escrow should release funds. - function isConditionMet(uint256 escrowId) external view returns (bool); + /// @notice Check if the release condition for an escrow is met. + /// @dev Called on every redeem attempt. MUST be a view function. + /// @param escrowId The sequential escrow identifier. + /// @return True if the escrow should release funds. + function isConditionMet(uint256 escrowId) external view returns (bool); - /// @notice Initialize condition configuration for a new escrow. - /// @dev Called atomically during ConfidentialEscrow.create(). - /// @param escrowId The sequential escrow identifier. - /// @param data ABI-encoded configuration specific to this resolver. - function onConditionSet(uint256 escrowId, bytes calldata data) external; + /// @notice Initialize condition configuration for a new escrow. + /// @dev Called atomically during ConfidentialEscrow.create(). + /// @param escrowId The sequential escrow identifier. + /// @param data ABI-encoded configuration specific to this resolver. + function onConditionSet(uint256 escrowId, bytes calldata data) external; } diff --git a/contracts/interfaces/IUnderwriterPolicy.sol b/contracts/interfaces/IUnderwriterPolicy.sol index 759672a..309fcdc 100644 --- a/contracts/interfaces/IUnderwriterPolicy.sol +++ b/contracts/interfaces/IUnderwriterPolicy.sol @@ -8,23 +8,23 @@ import {euint64, ebool} from "@fhenixprotocol/cofhe-contracts/FHE.sol"; /// @dev Implement this to define risk evaluation and dispute resolution. /// Return values are FHE-encrypted — the protocol operates on ciphertexts. interface IUnderwriterPolicy { - /// @notice Initialize policy-specific data for a new coverage. - /// @param coverageId The coverage identifier. - /// @param data ABI-encoded policy configuration. - function onPolicySet(uint256 coverageId, bytes calldata data) external; + /// @notice Initialize policy-specific data for a new coverage. + /// @param coverageId The coverage identifier. + /// @param data ABI-encoded policy configuration. + function onPolicySet(uint256 coverageId, bytes calldata data) external; - /// @notice Evaluate risk and return an encrypted risk score. - /// @dev Score is in basis points (0-10000). 100 bps = 1% premium. - /// MUST call FHE.allowThis() and FHE.allow(value, msg.sender) on return value. - /// @param escrowId The escrow being insured. - /// @param riskProof Arbitrary proof data for risk evaluation. - /// @return riskScore Encrypted risk score in basis points. - function evaluateRisk(uint256 escrowId, bytes calldata riskProof) external returns (euint64 riskScore); + /// @notice Evaluate risk and return an encrypted risk score. + /// @dev Score is in basis points (0-10000). 100 bps = 1% premium. + /// MUST call FHE.allowThis() and FHE.allow(value, msg.sender) on return value. + /// @param escrowId The escrow being insured. + /// @param riskProof Arbitrary proof data for risk evaluation. + /// @return riskScore Encrypted risk score in basis points. + function evaluateRisk(uint256 escrowId, bytes calldata riskProof) external returns (euint64 riskScore); - /// @notice Judge a dispute and return an encrypted verdict. - /// @dev MUST call FHE.allowThis() and FHE.allow(value, msg.sender) on return value. - /// @param coverageId The coverage being disputed. - /// @param disputeProof Arbitrary proof data from the claimant. - /// @return valid Encrypted boolean — true if the claim is legitimate. - function judge(uint256 coverageId, bytes calldata disputeProof) external returns (ebool valid); + /// @notice Judge a dispute and return an encrypted verdict. + /// @dev MUST call FHE.allowThis() and FHE.allow(value, msg.sender) on return value. + /// @param coverageId The coverage being disputed. + /// @param disputeProof Arbitrary proof data from the claimant. + /// @return valid Encrypted boolean — true if the claim is legitimate. + function judge(uint256 coverageId, bytes calldata disputeProof) external returns (ebool valid); } diff --git a/contracts/policies/SimpleUnderwriterPolicy.sol b/contracts/policies/SimpleUnderwriterPolicy.sol new file mode 100644 index 0000000..53ff092 --- /dev/null +++ b/contracts/policies/SimpleUnderwriterPolicy.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IUnderwriterPolicy} from "../interfaces/IUnderwriterPolicy.sol"; +import {FHE, euint64, ebool} from "@fhenixprotocol/cofhe-contracts/FHE.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/// @title SimpleUnderwriterPolicy +/// @notice Simple FHE-based insurance policy for testing +/// @dev Returns fixed risk scores and validates disputes +contract SimpleUnderwriterPolicy is IUnderwriterPolicy, ERC165 { + struct PolicyConfig { + uint64 baseRiskScore; + bool configured; + } + + mapping(uint256 => PolicyConfig) public policies; + + event PolicySet(uint256 indexed coverageId, uint64 baseRiskScore); + + error PolicyAlreadySet(); + + /// @inheritdoc IUnderwriterPolicy + function onPolicySet(uint256 coverageId, bytes calldata data) external { + if (policies[coverageId].configured) revert PolicyAlreadySet(); + + uint64 baseRiskScore = abi.decode(data, (uint64)); + require(baseRiskScore <= 10000, "Risk score must be <= 10000 bps"); + + policies[coverageId] = PolicyConfig({baseRiskScore: baseRiskScore, configured: true}); + + emit PolicySet(coverageId, baseRiskScore); + } + + /// @inheritdoc IUnderwriterPolicy + function evaluateRisk(uint256 coverageId, bytes calldata) external returns (euint64 riskScore) { + require(policies[coverageId].configured, "Policy not configured"); + + uint64 score = policies[coverageId].baseRiskScore; + euint64 encrypted = FHE.asEuint64(score); + + FHE.allowThis(encrypted); + FHE.allow(encrypted, msg.sender); + + return encrypted; + } + + /// @inheritdoc IUnderwriterPolicy + function judge(uint256 coverageId, bytes calldata disputeProof) external returns (ebool valid) { + require(policies[coverageId].configured, "Policy not configured"); + + // Simple validation: decode proof and check if valid + (bool isValid, uint256 timestamp) = abi.decode(disputeProof, (bool, uint256)); + + // Check if dispute is recent (within 30 days) + bool result = isValid && (block.timestamp - timestamp <= 30 days); + + ebool encrypted = FHE.asEbool(result); + FHE.allowThis(encrypted); + FHE.allow(encrypted, msg.sender); + + return encrypted; + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IUnderwriterPolicy).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/resolvers/TimeLockResolver.sol b/contracts/resolvers/TimeLockResolver.sol new file mode 100644 index 0000000..a8afa6b --- /dev/null +++ b/contracts/resolvers/TimeLockResolver.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IConditionResolver} from "../interfaces/IConditionResolver.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/// @title TimeLockResolver +/// @notice Simple time-based condition resolver for testing +/// @dev Releases escrow after a specified deadline +contract TimeLockResolver is IConditionResolver, ERC165 { + struct Config { + uint256 deadline; + } + + mapping(uint256 => Config) public configs; + + event ConditionSet(uint256 indexed escrowId, uint256 deadline); + + error InvalidDeadline(); + error ConditionAlreadySet(); + + /// @inheritdoc IConditionResolver + function onConditionSet(uint256 escrowId, bytes calldata data) external { + if (configs[escrowId].deadline != 0) revert ConditionAlreadySet(); + + uint256 deadline = abi.decode(data, (uint256)); + if (deadline <= block.timestamp) revert InvalidDeadline(); + + configs[escrowId] = Config({deadline: deadline}); + emit ConditionSet(escrowId, deadline); + } + + /// @inheritdoc IConditionResolver + function isConditionMet(uint256 escrowId) external view returns (bool) { + return block.timestamp >= configs[escrowId].deadline; + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IConditionResolver).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..db4cb0c --- /dev/null +++ b/foundry.lock @@ -0,0 +1,32 @@ +{ + "lib/cofhe-contracts": { + "tag": { + "name": "v0.1.3", + "rev": "cb76c99620e1b4acd395e00672be21e48702429f" + } + }, + "lib/cofhe-mock-contracts": { + "tag": { + "name": "v0.3.1", + "rev": "14e5446de3f130bac92b6cd181a24be4559957a0" + } + }, + "lib/forge-std": { + "tag": { + "name": "v1.15.0", + "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398" + } + }, + "lib/openzeppelin-contracts": { + "tag": { + "name": "v5.4.0", + "rev": "c64a1edb67b6e3f4a15cca8909c9482ad33a02b0" + } + }, + "lib/openzeppelin-foundry-upgrades": { + "tag": { + "name": "v0.4.0", + "rev": "cbce1e00305e943aa1661d43f41e5ac72c662b07" + } + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..d48f56c --- /dev/null +++ b/foundry.toml @@ -0,0 +1,32 @@ +[profile.default] +src = "contracts" +out = "out" +libs = ["lib"] +test = "test" +cache_path = "cache_forge" + +# Compiler settings matching Hardhat config +solc_version = "0.8.25" +evm_version = "cancun" +optimizer = true +optimizer_runs = 200 + +# Remappings for dependencies +remappings = [ + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@fhenixprotocol/cofhe-contracts/=lib/cofhe-contracts/contracts/", + "@fhenixprotocol/cofhe-mock-contracts/=lib/cofhe-mock-contracts/contracts/", + "openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/", + "forge-std/=lib/forge-std/src/" +] + +# RPC endpoints +[rpc_endpoints] +arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}" + +# Etherscan API keys for verification +[etherscan] +arbitrum_sepolia = { key = "${ETHERSCAN_API_KEY}" } + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/hardhat.config.ts b/hardhat.config.ts deleted file mode 100644 index 5c9bce8..0000000 --- a/hardhat.config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import '@nomicfoundation/hardhat-toolbox-viem'; -import '@nomicfoundation/hardhat-ethers'; -import 'cofhe-hardhat-plugin'; -import 'dotenv/config'; - -import type { HardhatUserConfig } from 'hardhat/config'; - -const PRIVATE_KEY = process.env.PRIVATE_KEY || '0x' + '0'.repeat(64); - -const config: HardhatUserConfig = { - solidity: { - version: '0.8.25', - settings: { - optimizer: { enabled: true, runs: 200 }, - evmVersion: 'cancun', - }, - }, - networks: { - hardhat: {}, - arbitrumSepolia: { - url: process.env.ARBITRUM_SEPOLIA_RPC_URL || 'https://sepolia-rollup.arbitrum.io/rpc', - chainId: 421614, - accounts: [PRIVATE_KEY], - }, - }, - etherscan: { - apiKey: { - arbitrumSepolia: process.env.ETHERSCAN_API_KEY || '', - }, - }, - mocha: { - timeout: 120000, - }, -}; - -export default config; diff --git a/package.json b/package.json index 09e917d..ebddda7 100644 --- a/package.json +++ b/package.json @@ -4,55 +4,29 @@ "reineira": { "platform": "0.1" }, - "description": "ReineiraOS plugin development environment with Claude Code agents", + "description": "ReineiraOS plugin development environment with Foundry", "private": true, "license": "MIT", "scripts": { - "compile": "hardhat compile", - "test": "hardhat test", - "test:resolvers": "hardhat test test/resolvers/", - "test:policies": "hardhat test test/policies/", - "clean": "hardhat clean", - "deploy": "hardhat run scripts/deploy.ts --network arbitrumSepolia", - "deploy:local": "hardhat run scripts/deploy.ts --network localhost", - "lint": "solhint 'contracts/**/*.sol'", - "format": "prettier --write '**/*.{ts,js,json,sol}'", - "format:check": "prettier --check '**/*.{ts,js,json,sol}'", - "prepare": "husky" + "build": "forge build", + "test": "forge test", + "test:verbose": "forge test -vvv", + "test:gas": "forge test --gas-report", + "clean": "forge clean", + "deploy:arbitrum": "forge script script/DeployTimeLockResolver.s.sol --rpc-url arbitrum_sepolia --broadcast --verify", + "deploy:local": "forge script script/DeployTimeLockResolver.s.sol --rpc-url http://localhost:8545 --broadcast", + "snapshot": "forge snapshot", + "format": "forge fmt", + "format:check": "forge fmt --check", + "anvil": "anvil" }, "dependencies": { - "@fhenixprotocol/cofhe-contracts": "0.0.13", - "@fhenixprotocol/cofhe-mock-contracts": "^0.3.1", - "@openzeppelin/contracts": "^5.4.0", - "@reineira-os/sdk": "^0.1.0", - "cofhe-hardhat-plugin": "^0.3.1", - "cofhejs": "^0.3.1" + "@reineira-os/sdk": "^0.1.0" }, "devDependencies": { - "@nomicfoundation/hardhat-chai-matchers": "^2.1.0", - "@nomicfoundation/hardhat-ethers": "~3.0.8", - "@nomicfoundation/hardhat-ignition-viem": "^3.0.9", - "@nomicfoundation/hardhat-network-helpers": "^1.1.2", - "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", - "@nomicfoundation/hardhat-verify": "^2.1.3", - "@nomicfoundation/hardhat-viem": "^2.1.3", - "@typechain/ethers-v6": "^0.5.1", - "@typechain/hardhat": "^9.1.0", - "@types/mocha": "^10.0.10", - "chai": "^4.5.0", "dotenv": "^16.4.0", - "ethers": "^6.16.0", - "hardhat": "~2.26.1", - "hardhat-gas-reporter": "^2.3.0", - "husky": "^9.1.7", - "lint-staged": "^16.4.0", "prettier": "^3.4.0", "prettier-plugin-solidity": "^1.4.0", - "solhint": "^5.0.0", - "solidity-coverage": "^0.8.17", - "ts-node": "^10.9.2", - "typescript": "^5.9.3", - "viem": "^2.47.4", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/git": "^10.0.1", @@ -60,14 +34,5 @@ "@semantic-release/release-notes-generator": "^14.1.0", "conventional-changelog-conventionalcommits": "^9.3.0", "semantic-release": "^25.0.3" - }, - "lint-staged": { - "*.sol": [ - "prettier --write", - "solhint" - ], - "*.{ts,js,json}": [ - "prettier --write" - ] } } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..c8e758a --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; + +/// @title Deploy +/// @notice Base deployment script for ReineiraOS plugins +/// @dev Extend this contract to deploy your resolvers or policies +abstract contract Deploy is Script { + /// @notice Deploy a contract to the configured network + /// @dev Override this function in your deployment script + function run() public virtual; + + /// @notice Get the deployer private key from environment + function getDeployerPrivateKey() internal view returns (uint256) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + require(deployerPrivateKey != 0, "PRIVATE_KEY not set in .env"); + return deployerPrivateKey; + } + + /// @notice Save deployment to JSON file + /// @param contractName Name of the deployed contract + /// @param contractAddress Address of the deployed contract + function saveDeployment(string memory contractName, address contractAddress) internal { + string memory network = getNetworkName(); + string memory deploymentPath = string.concat("deployments/", network, ".json"); + + // Create deployment record + string memory json = "deployment"; + vm.serializeString(json, "network", network); + vm.serializeAddress(json, "address", contractAddress); + vm.serializeAddress(json, "deployer", vm.addr(getDeployerPrivateKey())); + vm.serializeUint(json, "deployedAt", block.timestamp); + string memory finalJson = vm.serializeString(json, "contractName", contractName); + + // Try to write to file (may fail due to fs permissions in scripts) + try vm.writeJson(finalJson, deploymentPath, string.concat(".", contractName)) { + console2.log("Deployment saved to:", deploymentPath); + } catch { + console2.log("Note: Could not save deployment file (use --ffi flag if needed)"); + } + } + + /// @notice Get network name from chain ID + function getNetworkName() internal view returns (string memory) { + uint256 chainId = block.chainid; + if (chainId == 421614) return "arbitrumSepolia"; + if (chainId == 42161) return "arbitrum"; + if (chainId == 31337) return "localhost"; + return "unknown"; + } + + /// @notice Log deployment information + function logDeployment(string memory contractName, address contractAddress) internal view { + console2.log("\n=== Deployment Complete ==="); + console2.log("Contract:", contractName); + console2.log("Address:", contractAddress); + console2.log("Network:", getNetworkName()); + console2.log("Chain ID:", block.chainid); + console2.log("Deployer:", vm.addr(getDeployerPrivateKey())); + console2.log("\nNext steps:"); + console2.log(" Verify: forge verify-contract
--chain "); + console2.log(" Attach: Use the SDK to connect this contract to an escrow or insurance pool"); + console2.log("===========================\n"); + } +} diff --git a/script/DeploySimpleUnderwriterPolicy.s.sol b/script/DeploySimpleUnderwriterPolicy.s.sol new file mode 100644 index 0000000..b09e94f --- /dev/null +++ b/script/DeploySimpleUnderwriterPolicy.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Deploy} from "./Deploy.s.sol"; +import {SimpleUnderwriterPolicy} from "../contracts/policies/SimpleUnderwriterPolicy.sol"; + +contract DeploySimpleUnderwriterPolicy is Deploy { + function run() public override { + uint256 deployerPrivateKey = getDeployerPrivateKey(); + + vm.startBroadcast(deployerPrivateKey); + + SimpleUnderwriterPolicy policy = new SimpleUnderwriterPolicy(); + + vm.stopBroadcast(); + + logDeployment("SimpleUnderwriterPolicy", address(policy)); + saveDeployment("SimpleUnderwriterPolicy", address(policy)); + } +} diff --git a/script/DeployTimeLockResolver.s.sol b/script/DeployTimeLockResolver.s.sol new file mode 100644 index 0000000..c5f2b1b --- /dev/null +++ b/script/DeployTimeLockResolver.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Deploy} from "./Deploy.s.sol"; +import {TimeLockResolver} from "../contracts/resolvers/TimeLockResolver.sol"; + +contract DeployTimeLockResolver is Deploy { + function run() public override { + uint256 deployerPrivateKey = getDeployerPrivateKey(); + + vm.startBroadcast(deployerPrivateKey); + + TimeLockResolver resolver = new TimeLockResolver(); + + vm.stopBroadcast(); + + logDeployment("TimeLockResolver", address(resolver)); + saveDeployment("TimeLockResolver", address(resolver)); + } +} diff --git a/script/DeployUUPS.s.sol b/script/DeployUUPS.s.sol new file mode 100644 index 0000000..0955356 --- /dev/null +++ b/script/DeployUUPS.s.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +/// @title DeployUUPS +/// @notice Deployment script for UUPS upgradeable contracts +/// @dev Use this for deploying upgradeable resolvers or policies +abstract contract DeployUUPS is Script { + /// @notice Deploy a UUPS upgradeable contract + /// @dev Override this function in your deployment script + function run() public virtual; + + /// @notice Get the deployer private key from environment + function getDeployerPrivateKey() internal view returns (uint256) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + require(deployerPrivateKey != 0, "PRIVATE_KEY not set in .env"); + return deployerPrivateKey; + } + + /// @notice Deploy a UUPS proxy for a contract + /// @param contractName Name of the implementation contract + /// @param initializerData Encoded initializer function call + /// @return proxy Address of the deployed proxy + function deployUUPSProxy(string memory contractName, bytes memory initializerData) + internal + returns (address proxy) + { + vm.startBroadcast(getDeployerPrivateKey()); + + // Deploy UUPS proxy using OpenZeppelin Foundry Upgrades + proxy = Upgrades.deployUUPSProxy(contractName, initializerData); + + vm.stopBroadcast(); + + logDeployment(contractName, proxy); + saveDeployment(contractName, proxy); + + return proxy; + } + + /// @notice Upgrade a UUPS proxy to a new implementation + /// @param proxyAddress Address of the existing proxy + /// @param newContractName Name of the new implementation contract + /// @param initializerData Encoded initializer function call for the upgrade + function upgradeUUPSProxy(address proxyAddress, string memory newContractName, bytes memory initializerData) + internal + { + vm.startBroadcast(getDeployerPrivateKey()); + + Upgrades.upgradeProxy(proxyAddress, newContractName, initializerData); + + vm.stopBroadcast(); + + console2.log("\n=== Upgrade Complete ==="); + console2.log("Proxy:", proxyAddress); + console2.log("New Implementation:", newContractName); + console2.log("===========================\n"); + } + + /// @notice Save deployment to JSON file + /// @param contractName Name of the deployed contract + /// @param proxyAddress Address of the deployed proxy + function saveDeployment(string memory contractName, address proxyAddress) internal { + string memory network = getNetworkName(); + string memory deploymentPath = string.concat("deployments/", network, ".json"); + + // Create deployment record + string memory json = "deployment"; + vm.serializeString(json, "network", network); + vm.serializeAddress(json, "proxy", proxyAddress); + vm.serializeAddress(json, "deployer", vm.addr(getDeployerPrivateKey())); + vm.serializeUint(json, "deployedAt", block.timestamp); + vm.serializeString(json, "type", "UUPS"); + string memory finalJson = vm.serializeString(json, "contractName", contractName); + + // Try to write to file (may fail due to fs permissions in scripts) + try vm.writeJson(finalJson, deploymentPath, string.concat(".", contractName)) { + console2.log("Deployment saved to:", deploymentPath); + } catch { + console2.log("Note: Could not save deployment file (use --ffi flag if needed)"); + } + } + + /// @notice Get network name from chain ID + function getNetworkName() internal view returns (string memory) { + uint256 chainId = block.chainid; + if (chainId == 421614) return "arbitrumSepolia"; + if (chainId == 42161) return "arbitrum"; + if (chainId == 31337) return "localhost"; + return "unknown"; + } + + /// @notice Log deployment information + function logDeployment(string memory contractName, address proxyAddress) internal view { + console2.log("\n=== UUPS Proxy Deployment Complete ==="); + console2.log("Contract:", contractName); + console2.log("Proxy Address:", proxyAddress); + console2.log("Network:", getNetworkName()); + console2.log("Chain ID:", block.chainid); + console2.log("Deployer:", vm.addr(getDeployerPrivateKey())); + console2.log("\nNext steps:"); + console2.log(" Verify: forge verify-contract --chain "); + console2.log(" Attach: Use the SDK to connect this contract to an escrow or insurance pool"); + console2.log("===========================\n"); + } +} diff --git a/scripts/deploy.ts b/scripts/deploy.ts deleted file mode 100644 index 775db89..0000000 --- a/scripts/deploy.ts +++ /dev/null @@ -1,68 +0,0 @@ -import hre from 'hardhat'; -import fs from 'fs'; -import path from 'path'; - -async function main() { - const contractName = process.env.CONTRACT_NAME; - - if (!contractName) { - // List available contracts - const resolvers = fs.readdirSync('contracts/resolvers').filter((f) => f.endsWith('.sol')); - const policies = fs.readdirSync('contracts/policies').filter((f) => f.endsWith('.sol')); - - console.log('\nAvailable contracts:'); - if (resolvers.length) { - console.log(' Resolvers:', resolvers.map((f) => f.replace('.sol', '')).join(', ')); - } - if (policies.length) { - console.log(' Policies:', policies.map((f) => f.replace('.sol', '')).join(', ')); - } - if (!resolvers.length && !policies.length) { - console.log(' (none — create a contract first)'); - } - - console.log('\nUsage: CONTRACT_NAME=MyResolver npx hardhat run scripts/deploy.ts --network arbitrumSepolia\n'); - return; - } - - console.log(`\nDeploying ${contractName} to ${hre.network.name}...`); - - const [deployer] = await hre.viem.getWalletClients(); - console.log('Deployer:', deployer.account.address); - - const contract = await hre.viem.deployContract(contractName); - const address = contract.address; - - console.log(`${contractName} deployed to: ${address}`); - - // Save deployment record - const deploymentsDir = path.join(__dirname, '..', 'deployments'); - const deploymentsFile = path.join(deploymentsDir, `${hre.network.name}.json`); - - let deployments: Record = {}; - if (fs.existsSync(deploymentsFile)) { - deployments = JSON.parse(fs.readFileSync(deploymentsFile, 'utf-8')); - } - - const record = { - ...deployments, - [contractName]: { - address, - deployer: deployer.account.address, - deployedAt: new Date().toISOString(), - network: hre.network.name, - }, - }; - - fs.writeFileSync(deploymentsFile, JSON.stringify(record, null, 2)); - console.log(`Deployment saved to ${deploymentsFile}`); - - console.log('\nNext steps:'); - console.log(` Verify: npx hardhat verify --network ${hre.network.name} ${address}`); - console.log(` Attach: Use the SDK to connect this contract to an escrow or insurance pool`); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/test/SimpleUnderwriterPolicy.t.sol b/test/SimpleUnderwriterPolicy.t.sol new file mode 100644 index 0000000..4688bbc --- /dev/null +++ b/test/SimpleUnderwriterPolicy.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {SimpleUnderwriterPolicy} from "../contracts/policies/SimpleUnderwriterPolicy.sol"; +import {IUnderwriterPolicy} from "../contracts/interfaces/IUnderwriterPolicy.sol"; + +contract SimpleUnderwriterPolicyTest is Test { + SimpleUnderwriterPolicy public policy; + + uint256 constant COVERAGE_ID = 1; + uint256 constant ESCROW_ID = 100; + uint64 constant BASE_RISK_SCORE = 500; // 5% + + function setUp() public { + policy = new SimpleUnderwriterPolicy(); + + // Warp to a reasonable timestamp to avoid underflow + vm.warp(100 days); + } + + function test_OnPolicySet() public { + bytes memory data = abi.encode(BASE_RISK_SCORE); + + vm.expectEmit(true, false, false, true); + emit SimpleUnderwriterPolicy.PolicySet(COVERAGE_ID, BASE_RISK_SCORE); + + policy.onPolicySet(COVERAGE_ID, data); + + (uint64 storedScore, bool configured) = policy.policies(COVERAGE_ID); + assertEq(storedScore, BASE_RISK_SCORE); + assertTrue(configured); + } + + function test_OnPolicySet_RevertsIfAlreadySet() public { + bytes memory data = abi.encode(BASE_RISK_SCORE); + policy.onPolicySet(COVERAGE_ID, data); + + vm.expectRevert(SimpleUnderwriterPolicy.PolicyAlreadySet.selector); + policy.onPolicySet(COVERAGE_ID, data); + } + + function test_OnPolicySet_RevertsIfScoreTooHigh() public { + uint64 invalidScore = 10001; + bytes memory data = abi.encode(invalidScore); + + vm.expectRevert("Risk score must be <= 10000 bps"); + policy.onPolicySet(COVERAGE_ID, data); + } + + // Note: FHE operations (evaluateRisk, judge) require the CoFHE precompile + // which is only available on Fhenix/FHE-enabled networks, not in standard Foundry tests. + // These functions are tested in the TypeScript integration tests or on testnet. + + function test_EvaluateRisk_RevertsIfNotConfigured() public { + bytes memory riskProof = ""; + + vm.expectRevert("Policy not configured"); + policy.evaluateRisk(COVERAGE_ID, riskProof); + } + + function test_Judge_RevertsIfNotConfigured() public { + bytes memory disputeProof = abi.encode(true, block.timestamp); + + vm.expectRevert("Policy not configured"); + policy.judge(COVERAGE_ID, disputeProof); + } + + function test_SupportsInterface() public view { + bytes4 policyInterface = type(IUnderwriterPolicy).interfaceId; + assertTrue(policy.supportsInterface(policyInterface)); + } + + function testFuzz_OnPolicySet(uint64 riskScore) public { + vm.assume(riskScore <= 10000); + + bytes memory data = abi.encode(riskScore); + policy.onPolicySet(COVERAGE_ID, data); + + (uint64 storedScore, bool configured) = policy.policies(COVERAGE_ID); + assertEq(storedScore, riskScore); + assertTrue(configured); + } +} diff --git a/test/TimeLockResolver.t.sol b/test/TimeLockResolver.t.sol new file mode 100644 index 0000000..e3af34c --- /dev/null +++ b/test/TimeLockResolver.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {TimeLockResolver} from "../contracts/resolvers/TimeLockResolver.sol"; +import {IConditionResolver} from "../contracts/interfaces/IConditionResolver.sol"; + +contract TimeLockResolverTest is Test { + TimeLockResolver public resolver; + + uint256 constant ESCROW_ID = 1; + uint256 deadline; + + function setUp() public { + resolver = new TimeLockResolver(); + deadline = block.timestamp + 1 days; + } + + function test_OnConditionSet() public { + bytes memory data = abi.encode(deadline); + + vm.expectEmit(true, false, false, true); + emit TimeLockResolver.ConditionSet(ESCROW_ID, deadline); + + resolver.onConditionSet(ESCROW_ID, data); + + (uint256 storedDeadline) = resolver.configs(ESCROW_ID); + assertEq(storedDeadline, deadline); + } + + function test_OnConditionSet_RevertsIfPastDeadline() public { + uint256 pastDeadline = block.timestamp - 1; + bytes memory data = abi.encode(pastDeadline); + + vm.expectRevert(TimeLockResolver.InvalidDeadline.selector); + resolver.onConditionSet(ESCROW_ID, data); + } + + function test_OnConditionSet_RevertsIfAlreadySet() public { + bytes memory data = abi.encode(deadline); + resolver.onConditionSet(ESCROW_ID, data); + + vm.expectRevert(TimeLockResolver.ConditionAlreadySet.selector); + resolver.onConditionSet(ESCROW_ID, data); + } + + function test_IsConditionMet_ReturnsFalseBeforeDeadline() public { + bytes memory data = abi.encode(deadline); + resolver.onConditionSet(ESCROW_ID, data); + + assertFalse(resolver.isConditionMet(ESCROW_ID)); + } + + function test_IsConditionMet_ReturnsTrueAfterDeadline() public { + bytes memory data = abi.encode(deadline); + resolver.onConditionSet(ESCROW_ID, data); + + vm.warp(deadline); + assertTrue(resolver.isConditionMet(ESCROW_ID)); + } + + function test_IsConditionMet_ReturnsTrueAfterDeadlinePassed() public { + bytes memory data = abi.encode(deadline); + resolver.onConditionSet(ESCROW_ID, data); + + vm.warp(deadline + 1 hours); + assertTrue(resolver.isConditionMet(ESCROW_ID)); + } + + function test_SupportsInterface() public view { + bytes4 resolverInterface = type(IConditionResolver).interfaceId; + assertTrue(resolver.supportsInterface(resolverInterface)); + } + + function testFuzz_OnConditionSet(uint256 futureDeadline) public { + vm.assume(futureDeadline > block.timestamp); + vm.assume(futureDeadline < type(uint256).max); + + bytes memory data = abi.encode(futureDeadline); + resolver.onConditionSet(ESCROW_ID, data); + + (uint256 storedDeadline) = resolver.configs(ESCROW_ID); + assertEq(storedDeadline, futureDeadline); + } +} diff --git a/test/policies/.gitkeep b/test/policies/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/resolvers/.gitkeep b/test/resolvers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 0b475c3..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "resolveJsonModule": true - }, - "include": ["./scripts", "./test", "./hardhat.config.ts"], - "files": ["./hardhat.config.ts"] -}