From f3d2961b9747fe6010f5e3303905f28b61c20a89 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Fri, 24 Apr 2026 12:56:40 -0700 Subject: [PATCH] feat: add hybx omnl stack and gas pmm tooling --- .github/workflows/hybx-omnl-ts.yml | 71 +++ .github/workflows/omnl-reconcile.yml | 67 +++ config/chain138-eth-pmm-liquidity-plan.json | 54 ++ .../chain138-eth-pmm-pool-deployment-map.json | 142 +++++ config/chain138-eth-pmm-pools-execution.json | 67 +++ config/config-rpc-thirdweb-admin-core.toml | 11 +- config/deployment-omnl.example.env | 40 ++ config/gas-pmm-execution-bundle.json | 548 ++++++++++++++++++ .../hybx-omnl-cross-chain-lines.example.json | 20 + config/hybx-omnl-cross-chain-lines.json | 4 + config/hybx-omnl-policy.json | 22 + config/omnl-ipsas-gl-registry.json | 60 ++ config/omnl-journal-matrix.json | 99 ++++ ...-cronos-dodo-4-pools-execution-bundle.json | 97 ++++ contracts/hybx-omnl/ComplianceCore.sol | 162 ++++++ contracts/hybx-omnl/InstrumentRegistry.sol | 92 +++ contracts/hybx-omnl/OMNLCircuitBreaker.sol | 53 ++ contracts/hybx-omnl/OMNLMirrorCoordinator.sol | 109 ++++ contracts/hybx-omnl/OMNLMirrorReceiver.sol | 80 +++ contracts/hybx-omnl/PolicyMath.sol | 52 ++ .../hybx-omnl/ReserveCommitmentStore.sol | 175 ++++++ .../interfaces/IZkReserveProofVerifier.sol | 12 + docs/hybx-omnl/CCIP_MIRROR_FLOW.md | 41 ++ docs/hybx-omnl/DEPLOYMENT_CHECKLIST.md | 24 + docs/hybx-omnl/EXTERNAL_AUDIT_CHECKLIST.md | 31 + docs/hybx-omnl/HYBX_OMNL_POLICY_SPEC.md | 54 ++ docs/hybx-omnl/OMNL_IPSAS_API.md | 67 +++ docs/hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md | 55 ++ docs/hybx-omnl/OPERATIONAL_COMPLIANCE.md | 45 ++ docs/hybx-omnl/README.md | 24 + docs/hybx-omnl/RUNBOOK_CIRCUIT_BREAKERS.md | 30 + docs/hybx-omnl/SECURITY_THREAT_MODEL.md | 30 + docs/hybx-omnl/ZK_INTEGRATION.md | 9 + foundry.toml | 7 + .../hybx-omnl/DeployMirrorCoordinator.s.sol | 25 + script/hybx-omnl/DeployOMNLStack.s.sol | 30 + .../build-gas-pmm-execution-bundle.sh | 404 +++++++++++++ ...ild-optimism-cronos-dodo-4-pools-bundle.sh | 284 +++++++++ .../deployment/c138-cw-bridge-mainnet-pct.sh | 232 ++++++++ .../configure-cw-public-bridge-mesh.sh | 231 ++++++++ .../deployment/create-uniswap-v3-gas-pool.sh | 81 +++ .../cw-enforce-bridge-only-roles.sh | 299 ++++++++++ .../deploy-chain138-gas-canonicals.sh | 54 ++ .../deployment/fund-chain138-eth-pmm-pools.sh | 297 ++++++++++ .../deployment/fund-uniswap-v3-gas-pool.sh | 112 ++++ .../verify-all-networks-explorers.sh | 66 +++ .../deployment/verify-mainnet-cw-etherscan.sh | 105 ++++ .../verify-multichain-cw-etherscan.sh | 130 +++++ scripts/hybx-omnl/ci-omnl-validation.sh | 14 + scripts/hybx-omnl/omnl-reconcile-artifact.sh | 40 ++ scripts/hybx-omnl/sync-to-publish.sh | 66 +++ .../hybx-omnl/validate-cross-chain-config.mjs | 53 ++ scripts/hybx-omnl/verify-deployment.sh | 12 + .../public/omnl-dashboard.html | 47 ++ .../scripts/encode-omnl-mirror-payload.mjs | 20 + .../scripts/omnl-attestation-payload.mjs | 22 + .../scripts/omnl-reconcile-report.mjs | 34 ++ .../scripts/omnl-reconcile-stub.mjs | 13 + .../scripts/omnl-ttl-monitor.mjs | 51 ++ .../src/api/middleware/omnl-guards.test.ts | 37 ++ .../src/api/middleware/omnl-guards.ts | 21 + .../src/api/routes/omnl-ipsas.ts | 222 +++++++ .../token-aggregation/src/api/routes/omnl.ts | 324 +++++++++++ .../src/indexer/omnl-event-poller.ts | 86 +++ .../src/indexer/omnl-poller-state.ts | 41 ++ .../src/resources/omnl-openapi.json | 276 +++++++++ .../src/services/omnl-api-catalog.ts | 50 ++ .../src/services/omnl-compliance.ts | 274 +++++++++ .../src/services/omnl-integration-status.ts | 31 + .../src/services/omnl-ipsas-gl.ts | 210 +++++++ .../src/services/omnl-journal-matrix.ts | 43 ++ .../src/services/omnl-policy-math.ts | 19 + .../services/omnl-reconcile-anchor.test.ts | 11 + .../src/services/omnl-reconcile-anchor.ts | 60 ++ .../src/services/omnl-webhooks.test.ts | 18 + .../src/services/omnl-webhooks.ts | 69 +++ test/flash/EstimateMainnetCwUnwindFork.t.sol | 60 ++ test/hybx-omnl/ComplianceCore.t.sol | 89 +++ test/hybx-omnl/PolicyMath.t.sol | 24 + test/hybx-omnl/ReserveAttestation.t.sol | 53 ++ 80 files changed, 7192 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/hybx-omnl-ts.yml create mode 100644 .github/workflows/omnl-reconcile.yml create mode 100644 config/chain138-eth-pmm-liquidity-plan.json create mode 100644 config/chain138-eth-pmm-pool-deployment-map.json create mode 100644 config/chain138-eth-pmm-pools-execution.json create mode 100644 config/deployment-omnl.example.env create mode 100644 config/gas-pmm-execution-bundle.json create mode 100644 config/hybx-omnl-cross-chain-lines.example.json create mode 100644 config/hybx-omnl-cross-chain-lines.json create mode 100644 config/hybx-omnl-policy.json create mode 100644 config/omnl-ipsas-gl-registry.json create mode 100644 config/omnl-journal-matrix.json create mode 100644 config/optimism-cronos-dodo-4-pools-execution-bundle.json create mode 100644 contracts/hybx-omnl/ComplianceCore.sol create mode 100644 contracts/hybx-omnl/InstrumentRegistry.sol create mode 100644 contracts/hybx-omnl/OMNLCircuitBreaker.sol create mode 100644 contracts/hybx-omnl/OMNLMirrorCoordinator.sol create mode 100644 contracts/hybx-omnl/OMNLMirrorReceiver.sol create mode 100644 contracts/hybx-omnl/PolicyMath.sol create mode 100644 contracts/hybx-omnl/ReserveCommitmentStore.sol create mode 100644 contracts/hybx-omnl/interfaces/IZkReserveProofVerifier.sol create mode 100644 docs/hybx-omnl/CCIP_MIRROR_FLOW.md create mode 100644 docs/hybx-omnl/DEPLOYMENT_CHECKLIST.md create mode 100644 docs/hybx-omnl/EXTERNAL_AUDIT_CHECKLIST.md create mode 100644 docs/hybx-omnl/HYBX_OMNL_POLICY_SPEC.md create mode 100644 docs/hybx-omnl/OMNL_IPSAS_API.md create mode 100644 docs/hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md create mode 100644 docs/hybx-omnl/OPERATIONAL_COMPLIANCE.md create mode 100644 docs/hybx-omnl/README.md create mode 100644 docs/hybx-omnl/RUNBOOK_CIRCUIT_BREAKERS.md create mode 100644 docs/hybx-omnl/SECURITY_THREAT_MODEL.md create mode 100644 docs/hybx-omnl/ZK_INTEGRATION.md create mode 100644 script/hybx-omnl/DeployMirrorCoordinator.s.sol create mode 100644 script/hybx-omnl/DeployOMNLStack.s.sol create mode 100755 scripts/deployment/build-gas-pmm-execution-bundle.sh create mode 100755 scripts/deployment/build-optimism-cronos-dodo-4-pools-bundle.sh create mode 100755 scripts/deployment/c138-cw-bridge-mainnet-pct.sh create mode 100755 scripts/deployment/configure-cw-public-bridge-mesh.sh create mode 100755 scripts/deployment/create-uniswap-v3-gas-pool.sh create mode 100755 scripts/deployment/cw-enforce-bridge-only-roles.sh create mode 100644 scripts/deployment/deploy-chain138-gas-canonicals.sh create mode 100644 scripts/deployment/fund-chain138-eth-pmm-pools.sh create mode 100755 scripts/deployment/fund-uniswap-v3-gas-pool.sh create mode 100755 scripts/deployment/verify-all-networks-explorers.sh create mode 100755 scripts/deployment/verify-mainnet-cw-etherscan.sh create mode 100755 scripts/deployment/verify-multichain-cw-etherscan.sh create mode 100755 scripts/hybx-omnl/ci-omnl-validation.sh create mode 100755 scripts/hybx-omnl/omnl-reconcile-artifact.sh create mode 100755 scripts/hybx-omnl/sync-to-publish.sh create mode 100755 scripts/hybx-omnl/validate-cross-chain-config.mjs create mode 100755 scripts/hybx-omnl/verify-deployment.sh create mode 100644 services/token-aggregation/public/omnl-dashboard.html create mode 100644 services/token-aggregation/scripts/encode-omnl-mirror-payload.mjs create mode 100644 services/token-aggregation/scripts/omnl-attestation-payload.mjs create mode 100644 services/token-aggregation/scripts/omnl-reconcile-report.mjs create mode 100644 services/token-aggregation/scripts/omnl-reconcile-stub.mjs create mode 100644 services/token-aggregation/scripts/omnl-ttl-monitor.mjs create mode 100644 services/token-aggregation/src/api/middleware/omnl-guards.test.ts create mode 100644 services/token-aggregation/src/api/middleware/omnl-guards.ts create mode 100644 services/token-aggregation/src/api/routes/omnl-ipsas.ts create mode 100644 services/token-aggregation/src/api/routes/omnl.ts create mode 100644 services/token-aggregation/src/indexer/omnl-event-poller.ts create mode 100644 services/token-aggregation/src/indexer/omnl-poller-state.ts create mode 100644 services/token-aggregation/src/resources/omnl-openapi.json create mode 100644 services/token-aggregation/src/services/omnl-api-catalog.ts create mode 100644 services/token-aggregation/src/services/omnl-compliance.ts create mode 100644 services/token-aggregation/src/services/omnl-integration-status.ts create mode 100644 services/token-aggregation/src/services/omnl-ipsas-gl.ts create mode 100644 services/token-aggregation/src/services/omnl-journal-matrix.ts create mode 100644 services/token-aggregation/src/services/omnl-policy-math.ts create mode 100644 services/token-aggregation/src/services/omnl-reconcile-anchor.test.ts create mode 100644 services/token-aggregation/src/services/omnl-reconcile-anchor.ts create mode 100644 services/token-aggregation/src/services/omnl-webhooks.test.ts create mode 100644 services/token-aggregation/src/services/omnl-webhooks.ts create mode 100644 test/flash/EstimateMainnetCwUnwindFork.t.sol create mode 100644 test/hybx-omnl/ComplianceCore.t.sol create mode 100644 test/hybx-omnl/PolicyMath.t.sol create mode 100644 test/hybx-omnl/ReserveAttestation.t.sol diff --git a/.github/workflows/hybx-omnl-ts.yml b/.github/workflows/hybx-omnl-ts.yml new file mode 100644 index 0000000..43635c6 --- /dev/null +++ b/.github/workflows/hybx-omnl-ts.yml @@ -0,0 +1,71 @@ +# TypeScript build + IPSAS/journal anchor (no Forge — keeps PR feedback fast). +name: HYBX OMNL TypeScript & anchor + +on: + workflow_dispatch: + pull_request: + branches: [main, develop] + paths: + - 'contracts/hybx-omnl/**' + - 'services/token-aggregation/**' + - 'config/omnl-*.json' + - 'config/hybx-omnl-*.json' + - 'config/deployment-omnl.example.env' + - 'scripts/hybx-omnl/**' + - 'test/hybx-omnl/**' + - '.github/workflows/hybx-omnl-ts.yml' + - '.github/workflows/omnl-reconcile.yml' + push: + branches: [main, develop] + paths: + - 'contracts/hybx-omnl/**' + - 'services/token-aggregation/**' + - 'config/omnl-*.json' + - 'config/hybx-omnl-*.json' + - 'config/deployment-omnl.example.env' + - 'scripts/hybx-omnl/**' + - 'test/hybx-omnl/**' + - '.github/workflows/hybx-omnl-ts.yml' + - '.github/workflows/omnl-reconcile.yml' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ts-and-anchor: + name: token-aggregation build + reconcile artifact + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate cross-chain OMNL config + run: node scripts/hybx-omnl/validate-cross-chain-config.mjs + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: services/token-aggregation/package-lock.json + + - name: Install token-aggregation dependencies + working-directory: services/token-aggregation + run: npm ci + + - name: OMNL reconcile artifact + run: bash scripts/hybx-omnl/omnl-reconcile-artifact.sh + + - name: Build token-aggregation + working-directory: services/token-aggregation + run: npm run build + + - name: Upload reconcile artifacts + uses: actions/upload-artifact@v4 + with: + name: omnl-reconcile-pr-${{ github.run_id }} + path: artifacts/omnl-reconcile/ + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/omnl-reconcile.yml b/.github/workflows/omnl-reconcile.yml new file mode 100644 index 0000000..7569ad2 --- /dev/null +++ b/.github/workflows/omnl-reconcile.yml @@ -0,0 +1,67 @@ +name: OMNL reconcile anchor + +on: + workflow_dispatch: + schedule: + # Weekly Monday 07:05 UTC — adjust as needed + - cron: '5 7 * * 1' + pull_request: + branches: [main, develop] + paths: + - 'config/omnl-ipsas-gl-registry.json' + - 'config/omnl-journal-matrix.json' + - 'config/deployment-omnl.example.env' + - 'services/token-aggregation/scripts/omnl-reconcile-report.mjs' + - 'services/token-aggregation/package.json' + - 'scripts/hybx-omnl/omnl-reconcile-artifact.sh' + - '.github/workflows/omnl-reconcile.yml' + - '.github/workflows/hybx-omnl-ts.yml' + push: + branches: [main, develop] + paths: + - 'config/omnl-ipsas-gl-registry.json' + - 'config/omnl-journal-matrix.json' + - 'config/deployment-omnl.example.env' + - 'services/token-aggregation/scripts/omnl-reconcile-report.mjs' + - 'services/token-aggregation/package.json' + - 'scripts/hybx-omnl/omnl-reconcile-artifact.sh' + - '.github/workflows/omnl-reconcile.yml' + - '.github/workflows/hybx-omnl-ts.yml' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + reconcile: + name: Run omnl:reconcile and upload artifacts + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate cross-chain OMNL config + run: node scripts/hybx-omnl/validate-cross-chain-config.mjs + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Generate reconcile artifacts + env: + GITHUB_SHA: ${{ github.sha }} + GITHUB_RUN_ID: ${{ github.run_id }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: bash scripts/hybx-omnl/omnl-reconcile-artifact.sh + + - name: Upload OMNL reconcile artifacts + uses: actions/upload-artifact@v4 + with: + name: omnl-reconcile-${{ github.run_id }} + path: artifacts/omnl-reconcile/ + if-no-files-found: error + retention-days: 90 diff --git a/config/chain138-eth-pmm-liquidity-plan.json b/config/chain138-eth-pmm-liquidity-plan.json new file mode 100644 index 0000000..e176035 --- /dev/null +++ b/config/chain138-eth-pmm-liquidity-plan.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Planning inputs for funding the Chain 138 ETH-requested PMM surface, executed as WETH pools. The script derives the live USD per ETH anchor from the existing cUSDT/WETH pool and uses the FX assumptions below for non-USD GRU v2 assets.", + "version": "1.0.0", + "updated": "2026-04-20", + "executionConfig": "smom-dbis-138/config/chain138-eth-pmm-pools-execution.json", + "defaultProfile": "light", + "profiles": { + "light": { + "fiatBaseUnits": "100000000000", + "xauBaseUnits": "100000000" + }, + "medium": { + "fiatBaseUnits": "500000000000", + "xauBaseUnits": "250000000" + }, + "heavy": { + "fiatBaseUnits": "1000000000000", + "xauBaseUnits": "500000000" + } + }, + "usdPerUnit": { + "cUSDT": "1.00", + "cUSDC": "1.00", + "cEURC": "1.08", + "cEURT": "1.08", + "cGBPC": "1.27", + "cGBPT": "1.27", + "cAUDC": "0.64", + "cJPYC": "0.0067", + "cCHFC": "1.10", + "cCADC": "0.73" + }, + "xauPricing": { + "mode": "derive_from_public_pool", + "poolBaseSymbol": "cXAUC", + "poolQuoteSymbol": "cUSDC", + "fallbackUsdPerUnit": "3300.00", + "appliesTo": [ + "cXAUC", + "cXAUT" + ] + }, + "execution": { + "approveMax": true, + "wrapNativeEthWhenNeeded": true, + "mintMissingBaseWhenOwner": true, + "legacyGasPriceWei": "1000000000", + "addLiquidityGasLimit": "700000", + "approveGasLimit": "120000", + "mintGasLimit": "120000", + "wrapGasLimit": "120000" + } +} diff --git a/config/chain138-eth-pmm-pool-deployment-map.json b/config/chain138-eth-pmm-pool-deployment-map.json new file mode 100644 index 0000000..67067ff --- /dev/null +++ b/config/chain138-eth-pmm-pool-deployment-map.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Chain 138 deployment mapping for PMM pools quoted in native ETH across all canonical GRU v2 c* assets. Native ETH is the requested quote surface; execution uses WETH because the current DODO PMM flow is ERC-20 based.", + "version": "1.0.0", + "updated": "2026-04-20", + "chainId": 138, + "sourceOfTruth": { + "tokenConfig": "smom-dbis-138/config/chain138-pmm-pools.json", + "selectionRule": "All symbols in groups.cStarSymbols paired against native ETH request surface." + }, + "quoteAsset": { + "requestedSymbol": "ETH", + "requestedAssetType": "native", + "requestedAddress": null, + "executionSymbol": "WETH", + "executionAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "executionAssetType": "erc20_wrapped_native", + "deploymentRule": "Wrap native ETH to WETH before createPool/addLiquidity/registerPool operations.", + "notes": [ + "This mapping intentionally models ETH, not WETH, as the operator-facing quote asset.", + "Current Chain 138 DODO PMM tooling and on-chain pool contracts are ERC-20 based, so execution must use WETH." + ] + }, + "defaults": { + "lpFeeRate": 3, + "initialPrice": "1000000000000000000", + "kFactor": "500000000000000000", + "enableTwap": false, + "role": "public_routing", + "publicRoutingEnabled": true + }, + "pairs": [ + { + "baseSymbol": "cUSDT", + "baseAddress": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cUSDT/ETH", + "pairKeyExecution": "cUSDT/WETH" + }, + { + "baseSymbol": "cUSDC", + "baseAddress": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cUSDC/ETH", + "pairKeyExecution": "cUSDC/WETH" + }, + { + "baseSymbol": "cEURC", + "baseAddress": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cEURC/ETH", + "pairKeyExecution": "cEURC/WETH" + }, + { + "baseSymbol": "cEURT", + "baseAddress": "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cEURT/ETH", + "pairKeyExecution": "cEURT/WETH" + }, + { + "baseSymbol": "cGBPC", + "baseAddress": "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cGBPC/ETH", + "pairKeyExecution": "cGBPC/WETH" + }, + { + "baseSymbol": "cGBPT", + "baseAddress": "0x350f54e4D23795f86A9c03988c7135357CCaD97c", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cGBPT/ETH", + "pairKeyExecution": "cGBPT/WETH" + }, + { + "baseSymbol": "cAUDC", + "baseAddress": "0xD51482e567c03899eecE3CAe8a058161FD56069D", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cAUDC/ETH", + "pairKeyExecution": "cAUDC/WETH" + }, + { + "baseSymbol": "cJPYC", + "baseAddress": "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cJPYC/ETH", + "pairKeyExecution": "cJPYC/WETH" + }, + { + "baseSymbol": "cCHFC", + "baseAddress": "0x873990849DDa5117d7C644f0aF24370797C03885", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cCHFC/ETH", + "pairKeyExecution": "cCHFC/WETH" + }, + { + "baseSymbol": "cCADC", + "baseAddress": "0x54dBd40cF05e15906A2C21f600937e96787f5679", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cCADC/ETH", + "pairKeyExecution": "cCADC/WETH" + }, + { + "baseSymbol": "cXAUC", + "baseAddress": "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cXAUC/ETH", + "pairKeyExecution": "cXAUC/WETH" + }, + { + "baseSymbol": "cXAUT", + "baseAddress": "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", + "quoteSymbolRequested": "ETH", + "quoteSymbolExecution": "WETH", + "quoteAddressExecution": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "pairKeyRequested": "cXAUT/ETH", + "pairKeyExecution": "cXAUT/WETH" + } + ] +} diff --git a/config/chain138-eth-pmm-pools-execution.json b/config/chain138-eth-pmm-pools-execution.json new file mode 100644 index 0000000..a1fd0b8 --- /dev/null +++ b/config/chain138-eth-pmm-pools-execution.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Execution config for deploying native-ETH-requested PMM pairs on Chain 138. DODO PMM pools are ERC-20 based, so each ETH-requested pair is executed as a WETH pair.", + "version": "1.0.0", + "updated": "2026-04-20", + "chainId": 138, + "defaults": { + "lpFeeRate": 3, + "initialPrice": "1000000000000000000", + "kFactor": "500000000000000000", + "enableTwap": false + }, + "tokens": { + "cUSDT": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", + "cUSDC": "0xf22258f57794CC8E06237084b353Ab30fFfa640b", + "cEURC": "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", + "cEURT": "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", + "cGBPC": "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", + "cGBPT": "0x350f54e4D23795f86A9c03988c7135357CCaD97c", + "cAUDC": "0xD51482e567c03899eecE3CAe8a058161FD56069D", + "cJPYC": "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", + "cCHFC": "0x873990849DDa5117d7C644f0aF24370797C03885", + "cCADC": "0x54dBd40cF05e15906A2C21f600937e96787f5679", + "cXAUC": "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", + "cXAUT": "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", + "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "groups": { + "cStarSymbols": [ + "cUSDT", + "cUSDC", + "cEURC", + "cEURT", + "cGBPC", + "cGBPT", + "cAUDC", + "cJPYC", + "cCHFC", + "cCADC", + "cXAUC", + "cXAUT" + ], + "officialStableSymbols": [], + "wethSymbol": "WETH", + "deploy": { + "cStarVsCStar": false, + "cStarVsOfficial": false, + "cStarVsWeth": false, + "officialVsWeth": false + } + }, + "explicitPairs": [ + { "baseSymbol": "cUSDT", "quoteSymbol": "WETH" }, + { "baseSymbol": "cUSDC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cEURC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cEURT", "quoteSymbol": "WETH" }, + { "baseSymbol": "cGBPC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cGBPT", "quoteSymbol": "WETH" }, + { "baseSymbol": "cAUDC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cJPYC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cCHFC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cCADC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cXAUC", "quoteSymbol": "WETH" }, + { "baseSymbol": "cXAUT", "quoteSymbol": "WETH" } + ], + "plannedPairs": [] +} diff --git a/config/config-rpc-thirdweb-admin-core.toml b/config/config-rpc-thirdweb-admin-core.toml index 00cb80f..557e6dd 100644 --- a/config/config-rpc-thirdweb-admin-core.toml +++ b/config/config-rpc-thirdweb-admin-core.toml @@ -35,8 +35,15 @@ permissions-nodes-config-file="/var/lib/besu/permissions/permissions-nodes.toml" permissions-accounts-config-file-enabled=true permissions-accounts-config-file="/permissions/permissions-accounts.toml" -tx-pool-max-size=8192 -tx-pool-limit-by-account-percentage=0.5 +# Thirdweb (2103) only: allow at most ONE queued “next” future tx per sender in the layered pool. +# This rejects deep nonce gaps and huge sequential queues (e.g. 15) when chain next is 1, instead of +# retaining them until gossip refills. Use 1, not 0: tx-pool-max-future-by-sender=0 can trigger Besu +# LayeredPendingTransactions IndexOutOfBounds (SparseTransactions) on block import. +# Other RPC nodes (2101, 2201) stay at higher limits — see config-rpc-core / config-rpc-public. +tx-pool=layered +tx-pool-layer-max-capacity=12500000 +tx-pool-max-prioritized=2000 +tx-pool-max-future-by-sender=1 tx-pool-price-bump=10 bootnodes=[] diff --git a/config/deployment-omnl.example.env b/config/deployment-omnl.example.env new file mode 100644 index 0000000..4726dbb --- /dev/null +++ b/config/deployment-omnl.example.env @@ -0,0 +1,40 @@ +# HYBX OMNL — example deployment (copy; do not commit real secrets) +# Chain / RPC +CHAIN_ID=138 +OMNL_RPC_URL=https:// +OMNL_MIRROR_RPC_URL=https:// + +# Contracts (post-deploy) +OMNL_INSTRUMENT_REGISTRY= +OMNL_RESERVE_COMMITMENT_STORE= +OMNL_COMPLIANCE_CORE= +OMNL_CIRCUIT_BREAKER= +OMNL_MIRROR_RECEIVER= +OMNL_MIRROR_COORDINATOR= +OMNL_ZK_VERIFIER=0x0000000000000000000000000000000000000000 + +# CCIP +OMNL_CCIP_ROUTER= +OMNL_CCIP_DEST_CHAIN_SELECTOR= +OMNL_CCIP_FEE_TOKEN=0x0000000000000000000000000000000000000000 + +# Fineract / IPSAS +OMNL_FINERACT_BASE_URL= +OMNL_FINERACT_TENANT= +# Use OMNL_FINERACT_USER or legacy OMNL_FINERACT_USERNAME (same role) +OMNL_FINERACT_USER= +OMNL_FINERACT_USERNAME= +OMNL_FINERACT_PASSWORD= +OMNL_FINERACT_GL_PAGE_LIMIT=200 + +# API / ops +ENABLE_OMNL_EVENT_POLLER=true +OMNL_POLLER_STATE_PATH= +OMNL_WEBHOOK_URLS= +OMNL_WEBHOOK_SECRET= +OMNL_API_KEY= +OMNL_DASHBOARD_TOKEN= +OMNL_RATE_LIMIT_MAX=30 +OMNL_RATE_LIMIT_WINDOW_MS=60000 +OMNL_IPSAS_GL_REGISTRY= +OMNL_JOURNAL_MATRIX_PATH= diff --git a/config/gas-pmm-execution-bundle.json b/config/gas-pmm-execution-bundle.json new file mode 100644 index 0000000..66a6499 --- /dev/null +++ b/config/gas-pmm-execution-bundle.json @@ -0,0 +1,548 @@ +{ + "bundleName": "gas-pmm-all-22-pools", + "description": "Execution bundle for the remaining 22 planned gas-family DODO PMM placeholder rows across Ethereum mainnet and public networks.", + "poolDefaults": { + "lpFeeRate": 3, + "selfQuoteInitialPrice1e18": "1000000000000000000", + "k": "500000000000000000", + "enableTwap": true + }, + "chains": [ + { + "chainId": 1, + "network": "Ethereum Mainnet", + "chainKey": "ethereum", + "rpcEnv": [ + "ETHEREUM_MAINNET_RPC", + "MAINNET_RPC_URL", + "MAINNET_RPC" + ], + "integrationEnv": "CHAIN_1_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "ETHEREUM_DODO_VENDING_MACHINE_ADDRESS", + "MAINNET_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWETH", + "address": "0xf6dc5587e18f27adff60e303fdd98f35b50fa8a5" + }, + "wrappedNativeQuote": { + "symbol": "WETH", + "env": [ + "WETH9_MAINNET", + "WETH_MAINNET" + ], + "default": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "note": "Canonical mainnet WETH." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "ETHEREUM_OFFICIAL_USDC_ADDRESS", + "MAINNET_OFFICIAL_USDC_ADDRESS", + "OFFICIAL_USDC_MAINNET" + ], + "default": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "note": "Canonical mainnet USDC." + }, + "pairs": [ + { + "pair": "cWETH/WETH", + "baseSymbol": "cWETH", + "quoteSymbol": "WETH", + "expectedPoolAddress": "0xd011000000000000000000000000000000000001" + }, + { + "pair": "cWETH/USDC", + "baseSymbol": "cWETH", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd012000000000000000000000000000000000001" + } + ] + }, + { + "chainId": 10, + "network": "Optimism", + "chainKey": "optimism", + "rpcEnv": [ + "OPTIMISM_MAINNET_RPC", + "OPTIMISM_RPC_URL" + ], + "integrationEnv": "CHAIN_10_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "OPTIMISM_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWETHL2", + "address": "0x95007ec50d0766162f77848edf7bdc4eba147fb4" + }, + "wrappedNativeQuote": { + "symbol": "WETH", + "env": [ + "OPTIMISM_WETH_ADDRESS" + ], + "default": "0x4200000000000000000000000000000000000006", + "note": "Optimism canonical WETH." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "OPTIMISM_OFFICIAL_USDC_ADDRESS" + ], + "default": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "note": "Optimism native USDC." + }, + "pairs": [ + { + "pair": "cWETHL2/WETH", + "baseSymbol": "cWETHL2", + "quoteSymbol": "WETH", + "expectedPoolAddress": "0xd02100000000000000000000000000000000000a" + }, + { + "pair": "cWETHL2/USDC", + "baseSymbol": "cWETHL2", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd02200000000000000000000000000000000000a" + } + ] + }, + { + "chainId": 25, + "network": "Cronos", + "chainKey": "cronos", + "rpcEnv": [ + "CRONOS_RPC_URL", + "CRONOS_RPC", + "CRONOS_MAINNET_RPC" + ], + "integrationEnv": "CHAIN_25_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "CRONOS_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWCRO", + "address": "0x9b10eb0f77c45322dbd1fcb07176fd9a7609c164" + }, + "wrappedNativeQuote": { + "symbol": "WCRO", + "env": [ + "CRONOS_WCRO_ADDRESS", + "WCRO_ADDRESS" + ], + "default": "0x5C7F8A570d578ED84E63fdFA7b1eE72dEae1AE23", + "note": "Cronos wrapped CRO from the repo's multichain trustless deployment defaults." + }, + "stableQuote": { + "symbol": "USDT", + "env": [ + "CRONOS_OFFICIAL_USDT_ADDRESS" + ], + "default": "0x66e428c3f67a68878562e79A0234c1F83c208770", + "note": "Cronos USDT corrected to the official token list value already adopted in repo state." + }, + "pairs": [ + { + "pair": "cWCRO/WCRO", + "baseSymbol": "cWCRO", + "quoteSymbol": "WCRO", + "expectedPoolAddress": "0xd061000000000000000000000000000000000019" + }, + { + "pair": "cWCRO/USDT", + "baseSymbol": "cWCRO", + "quoteSymbol": "USDT", + "expectedPoolAddress": "0xd062000000000000000000000000000000000019" + } + ] + }, + { + "chainId": 56, + "network": "BSC (BNB Chain)", + "chainKey": "bsc", + "rpcEnv": [ + "BSC_RPC_URL", + "BSC_MAINNET_RPC" + ], + "integrationEnv": "CHAIN_56_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "BSC_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWBNB", + "address": "0x179034a08ac2c9c35d2e41239f68c79dca6f18fa" + }, + "wrappedNativeQuote": { + "symbol": "WBNB", + "env": [ + "BSC_WBNB_ADDRESS", + "WBNB_ADDRESS", + "BSC_WRAPPED_NATIVE_ADDRESS" + ], + "default": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + "note": "BSC canonical WBNB." + }, + "stableQuote": { + "symbol": "USDT", + "env": [ + "BSC_OFFICIAL_USDT_ADDRESS" + ], + "default": "0x55d398326f99059fF775485246999027B3197955", + "note": "BSC canonical USDT." + }, + "pairs": [ + { + "pair": "cWBNB/WBNB", + "baseSymbol": "cWBNB", + "quoteSymbol": "WBNB", + "expectedPoolAddress": "0xd031000000000000000000000000000000000038" + }, + { + "pair": "cWBNB/USDT", + "baseSymbol": "cWBNB", + "quoteSymbol": "USDT", + "expectedPoolAddress": "0xd032000000000000000000000000000000000038" + } + ] + }, + { + "chainId": 100, + "network": "Gnosis Chain", + "chainKey": "gnosis", + "rpcEnv": [ + "GNOSIS_RPC", + "GNOSIS_MAINNET_RPC" + ], + "integrationEnv": "CHAIN_100_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "GNOSIS_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWXDAI", + "address": "0x9f833b4f1012f52eb3317b09922a79c6edfca77d" + }, + "wrappedNativeQuote": { + "symbol": "WXDAI", + "env": [ + "WETH9_GNOSIS", + "GNOSIS_WXDAI_ADDRESS", + "WXDAI_ADDRESS" + ], + "default": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", + "note": "Gnosis wrapped xDAI from the config-ready chain env example." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "GNOSIS_OFFICIAL_USDC_ADDRESS" + ], + "default": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", + "note": "Gnosis canonical bridged USDC." + }, + "pairs": [ + { + "pair": "cWXDAI/WXDAI", + "baseSymbol": "cWXDAI", + "quoteSymbol": "WXDAI", + "expectedPoolAddress": "0xd071000000000000000000000000000000000064" + }, + { + "pair": "cWXDAI/USDC", + "baseSymbol": "cWXDAI", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd072000000000000000000000000000000000064" + } + ] + }, + { + "chainId": 137, + "network": "Polygon", + "chainKey": "polygon", + "rpcEnv": [ + "POLYGON_MAINNET_RPC", + "POLYGON_RPC_URL" + ], + "integrationEnv": "CHAIN_137_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "POLYGON_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWPOL", + "address": "0x25980244aacecb6d8c4b887261ed27f87cb2fc73" + }, + "wrappedNativeQuote": { + "symbol": "WPOL", + "env": [ + "POLYGON_WPOL_ADDRESS", + "POLYGON_WRAPPED_NATIVE_ADDRESS", + "WPOL_ADDRESS" + ], + "default": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "note": "Polygon native wrapped token contract; on-chain symbol may still read WMATIC." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "POLYGON_OFFICIAL_USDC_ADDRESS" + ], + "default": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "note": "Polygon native USDC." + }, + "pairs": [ + { + "pair": "cWPOL/WPOL", + "baseSymbol": "cWPOL", + "quoteSymbol": "WPOL", + "expectedPoolAddress": "0xd041000000000000000000000000000000000089" + }, + { + "pair": "cWPOL/USDC", + "baseSymbol": "cWPOL", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd042000000000000000000000000000000000089" + } + ] + }, + { + "chainId": 8453, + "network": "Base", + "chainKey": "base", + "rpcEnv": [ + "BASE_MAINNET_RPC", + "BASE_RPC_URL" + ], + "integrationEnv": "CHAIN_8453_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "BASE_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWETHL2", + "address": "0x2a0840e5117683b11682ac46f5cf5621e67269e3" + }, + "wrappedNativeQuote": { + "symbol": "WETH", + "env": [ + "BASE_WETH_ADDRESS" + ], + "default": "0x4200000000000000000000000000000000000006", + "note": "Base canonical WETH." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "BASE_OFFICIAL_USDC_ADDRESS" + ], + "default": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "note": "Base native USDC." + }, + "pairs": [ + { + "pair": "cWETHL2/WETH", + "baseSymbol": "cWETHL2", + "quoteSymbol": "WETH", + "expectedPoolAddress": "0xd021000000000000000000000000000000002105" + }, + { + "pair": "cWETHL2/USDC", + "baseSymbol": "cWETHL2", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd022000000000000000000000000000000002105" + } + ] + }, + { + "chainId": 42161, + "network": "Arbitrum One", + "chainKey": "arbitrum", + "rpcEnv": [ + "ARBITRUM_MAINNET_RPC", + "ARBITRUM_RPC" + ], + "integrationEnv": "CHAIN_42161_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "ARBITRUM_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWETHL2", + "address": "0xe27be001bc55cb2a8ed5ba5a62c834ca135244a3" + }, + "wrappedNativeQuote": { + "symbol": "WETH", + "env": [ + "ARBITRUM_WETH_ADDRESS" + ], + "default": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "note": "Arbitrum canonical WETH." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "ARBITRUM_OFFICIAL_USDC_ADDRESS" + ], + "default": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "note": "Arbitrum native USDC." + }, + "pairs": [ + { + "pair": "cWETHL2/WETH", + "baseSymbol": "cWETHL2", + "quoteSymbol": "WETH", + "expectedPoolAddress": "0xd02100000000000000000000000000000000a4b1" + }, + { + "pair": "cWETHL2/USDC", + "baseSymbol": "cWETHL2", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd02200000000000000000000000000000000a4b1" + } + ] + }, + { + "chainId": 42220, + "network": "Celo", + "chainKey": "celo", + "rpcEnv": [ + "CELO_RPC", + "CELO_MAINNET_RPC" + ], + "integrationEnv": "CHAIN_42220_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "CELO_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWCELO", + "address": "0xb0fa7ec4123c7c275b3a89d9239569707ea3c66a" + }, + "wrappedNativeQuote": { + "symbol": "WCELO", + "env": [ + "WETH9_CELO", + "CELO_WCELO_ADDRESS", + "WCELO_ADDRESS" + ], + "default": "0x2021B12D8138e2D63cF0895eccABC0DFc92416c6", + "note": "Celo wrapped CELO from the config-ready chain env example." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "CELO_OFFICIAL_USDC_ADDRESS" + ], + "default": "0xcebA9300f2b948710d2653dD7B07f33A8B32118C", + "note": "Celo USDC address currently used by the canonical deployment graph." + }, + "pairs": [ + { + "pair": "cWCELO/WCELO", + "baseSymbol": "cWCELO", + "quoteSymbol": "WCELO", + "expectedPoolAddress": "0xd08100000000000000000000000000000000a4ec" + }, + { + "pair": "cWCELO/USDC", + "baseSymbol": "cWCELO", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd08200000000000000000000000000000000a4ec" + } + ] + }, + { + "chainId": 43114, + "network": "Avalanche C-Chain", + "chainKey": "avalanche", + "rpcEnv": [ + "AVALANCHE_RPC_URL", + "AVALANCHE_MAINNET_RPC", + "AVALANCHE_RPC" + ], + "integrationEnv": "CHAIN_43114_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "AVALANCHE_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWAVAX", + "address": "0xe1d4aee2ef8f48a20338935188a8fe7f7c7de7d0" + }, + "wrappedNativeQuote": { + "symbol": "WAVAX", + "env": [ + "AVALANCHE_WAVAX_ADDRESS", + "WAVAX_ADDRESS" + ], + "default": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + "note": "Avalanche canonical WAVAX." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "AVALANCHE_OFFICIAL_USDC_ADDRESS" + ], + "default": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "note": "Avalanche native USDC." + }, + "pairs": [ + { + "pair": "cWAVAX/WAVAX", + "baseSymbol": "cWAVAX", + "quoteSymbol": "WAVAX", + "expectedPoolAddress": "0xd05100000000000000000000000000000000a86a" + }, + { + "pair": "cWAVAX/USDC", + "baseSymbol": "cWAVAX", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd05200000000000000000000000000000000a86a" + } + ] + }, + { + "chainId": 1111, + "network": "Wemix", + "chainKey": "wemix", + "rpcEnv": [ + "WEMIX_RPC", + "WEMIX_MAINNET_RPC" + ], + "integrationEnv": "CHAIN_1111_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": [ + "WEMIX_DODO_VENDING_MACHINE_ADDRESS" + ], + "baseToken": { + "symbol": "cWWEMIX", + "address": "0xc111000000000000000000000000000000000457" + }, + "wrappedNativeQuote": { + "symbol": "WWEMIX", + "env": [ + "WETH9_WEMIX", + "WEMIX_WWEMIX_ADDRESS", + "WWEMIX_ADDRESS" + ], + "default": "0x7D72b22a74A216Af4a002a1095C8C707d6eC1C5f", + "note": "Wemix wrapped native confirmed in the repo's verification docs." + }, + "stableQuote": { + "symbol": "USDC", + "env": [ + "WEMIX_OFFICIAL_USDC_ADDRESS" + ], + "default": "0xE3F5a90F9cb311505cd691a46596599aA1A0AD7D", + "note": "Recommended correction from the Wemix token verification document; this supersedes the placeholder-like master-map value." + }, + "pairs": [ + { + "pair": "cWWEMIX/WWEMIX", + "baseSymbol": "cWWEMIX", + "quoteSymbol": "WWEMIX", + "expectedPoolAddress": "0xd091000000000000000000000000000000000457" + }, + { + "pair": "cWWEMIX/USDC", + "baseSymbol": "cWWEMIX", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0xd092000000000000000000000000000000000457" + } + ] + } + ] +} diff --git a/config/hybx-omnl-cross-chain-lines.example.json b/config/hybx-omnl-cross-chain-lines.example.json new file mode 100644 index 0000000..ea8ae9d --- /dev/null +++ b/config/hybx-omnl-cross-chain-lines.example.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Map logical lineId to M0/M1 token addresses per chain for aggregated PoR (sum totalSupply across 138 + 651940). Copy to hybx-omnl-cross-chain-lines.json and set OMNL_CROSS_CHAIN_CONFIG.", + "version": "1.0.0", + "lines": [ + { + "lineId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "chains": { + "138": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0x0000000000000000000000000000000000000000" + }, + "651940": { + "tokenM0": "0x0000000000000000000000000000000000000000", + "tokenM1": "0x0000000000000000000000000000000000000000" + } + } + } + ] +} diff --git a/config/hybx-omnl-cross-chain-lines.json b/config/hybx-omnl-cross-chain-lines.json new file mode 100644 index 0000000..8be680e --- /dev/null +++ b/config/hybx-omnl-cross-chain-lines.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "lines": [] +} diff --git a/config/hybx-omnl-policy.json b/config/hybx-omnl-policy.json new file mode 100644 index 0000000..1b2a98c --- /dev/null +++ b/config/hybx-omnl-policy.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "HYBX OMNL deterministic policy constants (must match PolicyMath.sol / ComplianceCore.sol)", + "version": "1.0.0", + "m0BackingNumerator": 12, + "m0BackingDenominator": 10, + "m1ExpansionNumerator": 5, + "m1ExpansionDenominator": 1, + "rounding": { + "minReserves": "ceil_div", + "maxM1": "exact_mul" + }, + "aggregationScope": "per_line", + "chains": { + "primaryReserveWriter": 138, + "mirrorTarget": 651940 + }, + "defaultTtlSeconds": { + "fiat": 86400, + "xau": 14400 + } +} diff --git a/config/omnl-ipsas-gl-registry.json b/config/omnl-ipsas-gl-registry.json new file mode 100644 index 0000000..96daaf3 --- /dev/null +++ b/config/omnl-ipsas-gl-registry.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "IPSAS-aligned GL codes for OMNL Hybx (Fineract). Pairs must match omnl-journal-matrix.json postings. Source: docs/04-configuration/mifos-omnl-central-bank/OMNL_JOURNAL_LEDGER_MATRIX.md", + "version": "1.0.0", + "currencyCode": "USD", + "accounts": [ + { + "glCode": "1000", + "name": "USD Settlement & Reserve Assets", + "fineractType": "ASSET", + "usage": "Cash and cash equivalents (IPSAS 2); settlement balances", + "ipsasStandards": ["IPSAS 2", "IPSAS 28", "IPSAS 29"], + "roles": ["settlement", "cash_equivalent"] + }, + { + "glCode": "1050", + "name": "USD Treasury Conversion Reserve (M0)", + "fineractType": "ASSET", + "usage": "Reserve backing M1 capacity; financial asset", + "ipsasStandards": ["IPSAS 28", "IPSAS 29"], + "roles": ["m0_reserve", "treasury_reserve"] + }, + { + "glCode": "2000", + "name": "USD Central Deposits (M1)", + "fineractType": "LIABILITY", + "usage": "Demand deposits; financial liability", + "ipsasStandards": ["IPSAS 28", "IPSAS 29"], + "roles": ["m1_liability", "demand_deposit"] + }, + { + "glCode": "2100", + "name": "USD Restricted / Escrow (M1)", + "fineractType": "LIABILITY", + "usage": "Restricted liabilities; escrow", + "ipsasStandards": ["IPSAS 28", "IPSAS 29"], + "roles": ["m1_restricted", "escrow"] + }, + { + "glCode": "3000", + "name": "Opening Balance Control", + "fineractType": "EQUITY", + "usage": "Migration control; equity", + "ipsasStandards": ["IPSAS 1", "IPSAS 3"], + "roles": ["equity", "migration_control"] + } + ], + "allowedJournalPairs": [ + { "debitGlCode": "1000", "creditGlCode": "2000", "ipsasRef": "IPSAS 3, 28", "memo": "T-001" }, + { "debitGlCode": "1050", "creditGlCode": "2000", "ipsasRef": "IPSAS 28, 29", "memo": "T-001B" }, + { "debitGlCode": "2000", "creditGlCode": "2000", "ipsasRef": "IPSAS 28", "memo": "T-002A" }, + { "debitGlCode": "2000", "creditGlCode": "2100", "ipsasRef": "IPSAS 28", "memo": "T-002B" } + ], + "monetaryLayerHints": { + "m0_reserve": { "primaryGlCodes": ["1050"], "ipsasNarrative": "Treasury / M0 reserve assets (IPSAS 28, 29)" }, + "m1_liability": { "primaryGlCodes": ["2000", "2100"], "ipsasNarrative": "Financial liabilities — deposits (IPSAS 28, 29)" }, + "settlement": { "primaryGlCodes": ["1000"], "ipsasNarrative": "Cash and cash equivalents (IPSAS 2)" }, + "equity": { "primaryGlCodes": ["3000"], "ipsasNarrative": "Equity / control (IPSAS 1)" } + } +} diff --git a/config/omnl-journal-matrix.json b/config/omnl-journal-matrix.json new file mode 100644 index 0000000..cddaf0a --- /dev/null +++ b/config/omnl-journal-matrix.json @@ -0,0 +1,99 @@ +{ + "description": "OMNL journal entries for API posting to OMNL Hybx (Fineract). Head Office and entities 2–8 per Migration Memorandum. officeId=1 for all; narrative identifies entity. IPSAS-aligned.", + "source": "OMNL_JOURNAL_LEDGER_MATRIX.md", + "currencyCode": "USD", + "dateFormat": "yyyy-MM-dd", + "locale": "en", + "entries": [ + { + "memo": "T-001", + "officeId": 1, + "debitGlCode": "1000", + "creditGlCode": "2000", + "amount": 900000000000, + "narrative": "Opening Balance Migration (Head Office)", + "ipsasRef": "IPSAS 3, 28" + }, + { + "memo": "T-001B", + "officeId": 1, + "debitGlCode": "1050", + "creditGlCode": "2000", + "amount": 250000000000, + "narrative": "Treasury Conversion — Transfer to Reserve (M0); Head Office", + "ipsasRef": "IPSAS 28, 29" + }, + { + "memo": "T-002A", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2000", + "amount": 2900000000, + "narrative": "Shamrayan Available (M1) — Office 2", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-002B", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2100", + "amount": 2100000000, + "narrative": "Shamrayan Restricted — Office 2", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-003", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2100", + "amount": 350000000000, + "narrative": "HYBX Capitalization Escrow — Office 3", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-004", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2000", + "amount": 5000000000, + "narrative": "TAJ Allocation (M1) — Office 4", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-005", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2000", + "amount": 5000000000, + "narrative": "Aseret Allocation (M1) — Office 5", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-006", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2000", + "amount": 5000000000, + "narrative": "Mann Li Allocation (M1) — Office 6", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-007", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2000", + "amount": 50000000000, + "narrative": "OSJ Allocation (M1) — Office 7", + "ipsasRef": "IPSAS 28" + }, + { + "memo": "T-008", + "officeId": 1, + "debitGlCode": "2000", + "creditGlCode": "2000", + "amount": 50000000000, + "narrative": "Alltra Allocation (M1) — Office 8", + "ipsasRef": "IPSAS 28" + } + ] +} diff --git a/config/optimism-cronos-dodo-4-pools-execution-bundle.json b/config/optimism-cronos-dodo-4-pools-execution-bundle.json new file mode 100644 index 0000000..e0a8077 --- /dev/null +++ b/config/optimism-cronos-dodo-4-pools-execution-bundle.json @@ -0,0 +1,97 @@ +{ + "bundleName": "optimism-cronos-dodo-4-pools", + "description": "Focused execution bundle for the four remaining configured_no_code DODO PMM pools on Optimism and Cronos.", + "poolDefaults": { + "lpFeeRate": 3, + "initialPrice1e18": "1000000000000000000", + "k": "500000000000000000", + "enableTwap": true + }, + "chains": [ + { + "chainId": 10, + "network": "Optimism", + "rpcEnv": [ + "OPTIMISM_MAINNET_RPC", + "OPTIMISM_RPC_URL" + ], + "integrationEnv": "CHAIN_10_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": "OPTIMISM_DODO_VENDING_MACHINE_ADDRESS", + "official": { + "USDC": { + "env": "OPTIMISM_OFFICIAL_USDC_ADDRESS", + "default": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + }, + "USDT": { + "env": "OPTIMISM_OFFICIAL_USDT_ADDRESS", + "default": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58" + } + }, + "compliant": { + "cWUSDC": { + "env": "CWUSDC_OPTIMISM" + }, + "cWUSDT": { + "env": "CWUSDT_OPTIMISM" + } + }, + "pairs": [ + { + "pair": "cWUSDC/USDC", + "baseSymbol": "cWUSDC", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0x022a8835b5c8fd6714cE33c783a426398468702B" + }, + { + "pair": "cWUSDT/USDT", + "baseSymbol": "cWUSDT", + "quoteSymbol": "USDT", + "expectedPoolAddress": "0x0630059fc9a629DABAC1244c9f021A33A71B098f" + } + ] + }, + { + "chainId": 25, + "network": "Cronos", + "rpcEnv": [ + "CRONOS_RPC_URL", + "CRONOS_RPC", + "CRONOS_MAINNET_RPC" + ], + "integrationEnv": "CHAIN_25_DODO_PMM_INTEGRATION", + "dodoVendingMachineEnv": "CRONOS_DODO_VENDING_MACHINE_ADDRESS", + "official": { + "USDC": { + "env": "CRONOS_OFFICIAL_USDC_ADDRESS", + "default": "0xc21223249CA28397B4B6541dfFaEcC539BfF0c59" + }, + "USDT": { + "env": "CRONOS_OFFICIAL_USDT_ADDRESS", + "default": "0x66e428c3f67a68878562e79A0234c1F83c208770" + } + }, + "compliant": { + "cWUSDC": { + "env": "CWUSDC_CRONOS" + }, + "cWUSDT": { + "env": "CWUSDT_CRONOS" + } + }, + "pairs": [ + { + "pair": "cWUSDC/USDC", + "baseSymbol": "cWUSDC", + "quoteSymbol": "USDC", + "expectedPoolAddress": "0x72c50bb2c621a2C10E162776D0D210d3C9f8Ac02" + }, + { + "pair": "cWUSDT/USDT", + "baseSymbol": "cWUSDT", + "quoteSymbol": "USDT", + "expectedPoolAddress": "0xb4F3d4C8995032690837543438ac40BA5cbfd8Fe" + } + ] + } + ] +} diff --git a/contracts/hybx-omnl/ComplianceCore.sol b/contracts/hybx-omnl/ComplianceCore.sol new file mode 100644 index 0000000..28c4819 --- /dev/null +++ b/contracts/hybx-omnl/ComplianceCore.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {InstrumentRegistry} from "./InstrumentRegistry.sol"; +import {ReserveCommitmentStore} from "./ReserveCommitmentStore.sol"; +import {OMNLCircuitBreaker} from "./OMNLCircuitBreaker.sol"; +import {PolicyMath} from "./PolicyMath.sol"; + +/** + * @title ComplianceCore + * @notice Aggregates on-chain supply + attested reserves; enforces policy; emits audit events. + * @dev Integrated minters call assertCanMintM0/M1 before minting. + */ +contract ComplianceCore { + InstrumentRegistry public immutable registry; + ReserveCommitmentStore public immutable reserves; + OMNLCircuitBreaker public immutable breakers; + + event ComplianceSnapshot( + bytes32 indexed lineId, + uint256 s0, + uint256 s1, + uint256 r, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot, + bool m0Ok, + bool m1Ok, + bool attestationStale, + bool policyOk, + bool reportingCompliant + ); + error ComplianceBlocked(bytes32 lineId, string reason); + error UnknownLine(bytes32 lineId); + + constructor(address registry_, address reserves_, address breakers_) { + require(registry_ != address(0) && reserves_ != address(0) && breakers_ != address(0), "ComplianceCore: zero"); + registry = InstrumentRegistry(registry_); + reserves = ReserveCommitmentStore(reserves_); + breakers = OMNLCircuitBreaker(breakers_); + } + + function _loadSupplies(bytes32 lineId) internal view returns (uint256 s0, uint256 s1) { + InstrumentRegistry.Line memory line = registry.getLine(lineId); + if (line.tokenM0 == address(0)) revert UnknownLine(lineId); + s0 = IERC20(line.tokenM0).totalSupply(); + s1 = IERC20(line.tokenM1).totalSupply(); + } + + function _stale(uint256 validUntil) internal view returns (bool) { + return block.timestamp > validUntil; + } + + /** + * @notice Full deterministic compliance view for dashboards and off-chain API. + * @dev reportingCompliant is false when attestation is stale or breakers trip, even if pure math (policyOk) holds. + */ + function getCompliance(bytes32 lineId) + external + view + returns ( + uint256 s0, + uint256 s1, + uint256 r, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot, + uint256 minR, + uint256 maxS1, + bool m0Ok, + bool m1Ok, + bool attestationStale, + bool policyOk, + bool operational, + bool reportingCompliant + ) + { + (s0, s1) = _loadSupplies(lineId); + ReserveCommitmentStore.Commitment memory c = reserves.getCommitment(lineId); + r = c.R; + validUntil = c.validUntil; + evidenceHash = c.evidenceHash; + merkleRoot = c.merkleRoot; + minR = PolicyMath.minReservesForM0(s0); + maxS1 = PolicyMath.maxM1ForM0(s0); + attestationStale = _stale(validUntil); + m0Ok = PolicyMath.m0BackingOk(s0, r); + m1Ok = PolicyMath.m1ExpansionOk(s0, s1); + policyOk = PolicyMath.isCompliant(s0, s1, r); + operational = breakers.isLineOperational(lineId); + reportingCompliant = policyOk && !attestationStale && operational; + } + + /// @notice Reverts if global/line pause or policy would fail after minting delta M0. + function assertCanMintM0(bytes32 lineId, uint256 mintAmount) external view { + if (!breakers.isLineOperational(lineId)) revert ComplianceBlocked(lineId, "paused"); + InstrumentRegistry.Line memory line = registry.getLine(lineId); + if (line.tokenM0 == address(0)) revert UnknownLine(lineId); + if (!line.active) revert ComplianceBlocked(lineId, "inactive"); + + ReserveCommitmentStore.Commitment memory c = reserves.getCommitment(lineId); + if (breakers.enforceStaleBlockM0() && _stale(c.validUntil)) { + revert ComplianceBlocked(lineId, "stale_attestation"); + } + + uint256 s0 = IERC20(line.tokenM0).totalSupply(); + if (!PolicyMath.canMintM0(s0, mintAmount, c.R)) { + revert ComplianceBlocked(lineId, "m0_backing"); + } + } + + /// @notice Reverts if global/line pause or M1 cap exceeded after mint. + function assertCanMintM1(bytes32 lineId, uint256 mintAmount) external view { + if (!breakers.isLineOperational(lineId)) revert ComplianceBlocked(lineId, "paused"); + InstrumentRegistry.Line memory line = registry.getLine(lineId); + if (line.tokenM0 == address(0)) revert UnknownLine(lineId); + if (!line.active) revert ComplianceBlocked(lineId, "inactive"); + + ReserveCommitmentStore.Commitment memory c = reserves.getCommitment(lineId); + if (breakers.enforceStaleBlockM1() && _stale(c.validUntil)) { + revert ComplianceBlocked(lineId, "stale_attestation"); + } + + uint256 s0 = IERC20(line.tokenM0).totalSupply(); + uint256 s1 = IERC20(line.tokenM1).totalSupply(); + if (!PolicyMath.canMintM1(s0, s1, mintAmount)) { + revert ComplianceBlocked(lineId, "m1_cap"); + } + } + + /// @notice Permissionless heartbeat for indexers — emits current snapshot (costs gas for caller). + function emitComplianceSnapshot(bytes32 lineId) external { + InstrumentRegistry.Line memory line = registry.getLine(lineId); + if (line.tokenM0 == address(0)) revert UnknownLine(lineId); + + uint256 s0 = IERC20(line.tokenM0).totalSupply(); + uint256 s1 = IERC20(line.tokenM1).totalSupply(); + ReserveCommitmentStore.Commitment memory c = reserves.getCommitment(lineId); + bool stale_ = _stale(c.validUntil); + bool m0Ok = PolicyMath.m0BackingOk(s0, c.R); + bool m1Ok = PolicyMath.m1ExpansionOk(s0, s1); + bool policyOk = PolicyMath.isCompliant(s0, s1, c.R); + bool operational = breakers.isLineOperational(lineId); + bool reporting = policyOk && !stale_ && operational; + + emit ComplianceSnapshot( + lineId, + s0, + s1, + c.R, + c.validUntil, + c.evidenceHash, + c.merkleRoot, + m0Ok, + m1Ok, + stale_, + policyOk, + reporting + ); + } +} diff --git a/contracts/hybx-omnl/InstrumentRegistry.sol b/contracts/hybx-omnl/InstrumentRegistry.sol new file mode 100644 index 0000000..f89ca37 --- /dev/null +++ b/contracts/hybx-omnl/InstrumentRegistry.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title InstrumentRegistry + * @notice Maps lineId → M0/M1 ERC-20s, ISO-4217 metadata, and pause per line. + */ +contract InstrumentRegistry is AccessControl { + bytes32 public constant REGISTRY_ADMIN_ROLE = keccak256("REGISTRY_ADMIN_ROLE"); + + struct Line { + address tokenM0; + address tokenM1; + uint8 decimals; + uint16 iso4217Numeric; + bool isXAU; + bool active; + } + + mapping(bytes32 lineId => Line) private _lines; + bytes32[] private _allLineIds; + + event LineRegistered( + bytes32 indexed lineId, + address tokenM0, + address tokenM1, + uint8 decimals, + uint16 iso4217Numeric, + bool isXAU + ); + event LineUpdated(bytes32 indexed lineId, bool active); + event LineTokensUpdated(bytes32 indexed lineId, address tokenM0, address tokenM1); + + constructor(address admin) { + require(admin != address(0), "InstrumentRegistry: zero admin"); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(REGISTRY_ADMIN_ROLE, admin); + } + + function registerLine( + bytes32 lineId, + address tokenM0, + address tokenM1, + uint8 decimals_, + uint16 iso4217Numeric, + bool isXAU + ) external onlyRole(REGISTRY_ADMIN_ROLE) { + require(lineId != bytes32(0), "InstrumentRegistry: zero lineId"); + require(tokenM0 != address(0) && tokenM1 != address(0), "InstrumentRegistry: zero token"); + require(_lines[lineId].tokenM0 == address(0), "InstrumentRegistry: exists"); + + _lines[lineId] = Line({ + tokenM0: tokenM0, + tokenM1: tokenM1, + decimals: decimals_, + iso4217Numeric: iso4217Numeric, + isXAU: isXAU, + active: true + }); + + _allLineIds.push(lineId); + emit LineRegistered(lineId, tokenM0, tokenM1, decimals_, iso4217Numeric, isXAU); + } + + /// @notice All registered line ids (order of registration). + function allLineIds() external view returns (bytes32[] memory) { + return _allLineIds; + } + + function setLineActive(bytes32 lineId, bool active) external onlyRole(REGISTRY_ADMIN_ROLE) { + require(_lines[lineId].tokenM0 != address(0), "InstrumentRegistry: unknown line"); + _lines[lineId].active = active; + emit LineUpdated(lineId, active); + } + + function setLineTokens(bytes32 lineId, address tokenM0, address tokenM1) + external + onlyRole(REGISTRY_ADMIN_ROLE) + { + require(_lines[lineId].tokenM0 != address(0), "InstrumentRegistry: unknown line"); + require(tokenM0 != address(0) && tokenM1 != address(0), "InstrumentRegistry: zero token"); + _lines[lineId].tokenM0 = tokenM0; + _lines[lineId].tokenM1 = tokenM1; + emit LineTokensUpdated(lineId, tokenM0, tokenM1); + } + + function getLine(bytes32 lineId) external view returns (Line memory) { + return _lines[lineId]; + } +} diff --git a/contracts/hybx-omnl/OMNLCircuitBreaker.sol b/contracts/hybx-omnl/OMNLCircuitBreaker.sol new file mode 100644 index 0000000..8e70d54 --- /dev/null +++ b/contracts/hybx-omnl/OMNLCircuitBreaker.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title OMNLCircuitBreaker + * @notice Global / per-line pause; optional block on stale attestation for M1 mint path. + */ +contract OMNLCircuitBreaker is AccessControl { + bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + bool public globalPaused; + mapping(bytes32 lineId => bool) public linePaused; + /// @notice If true, M1 mint must revert when attestation is past validUntil (checked in ComplianceCore). + bool public enforceStaleBlockM1; + /// @notice If true, M0 mint must revert when attestation is stale. + bool public enforceStaleBlockM0; + + event GlobalPauseSet(bool paused, address indexed by); + event LinePauseSet(bytes32 indexed lineId, bool paused, address indexed by); + event StaleEnforcementSet(bool blockM0, bool blockM1); + + constructor(address admin) { + require(admin != address(0), "OMNLCircuitBreaker: zero admin"); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(GUARDIAN_ROLE, admin); + _grantRole(PAUSER_ROLE, admin); + } + + function setGlobalPaused(bool paused) external onlyRole(GUARDIAN_ROLE) { + globalPaused = paused; + emit GlobalPauseSet(paused, msg.sender); + } + + function setLinePaused(bytes32 lineId, bool paused) external onlyRole(PAUSER_ROLE) { + linePaused[lineId] = paused; + emit LinePauseSet(lineId, paused, msg.sender); + } + + function setStaleEnforcement(bool blockM0, bool blockM1) external onlyRole(DEFAULT_ADMIN_ROLE) { + enforceStaleBlockM0 = blockM0; + enforceStaleBlockM1 = blockM1; + emit StaleEnforcementSet(blockM0, blockM1); + } + + function isLineOperational(bytes32 lineId) external view returns (bool) { + if (globalPaused) return false; + if (linePaused[lineId]) return false; + return true; + } +} diff --git a/contracts/hybx-omnl/OMNLMirrorCoordinator.sol b/contracts/hybx-omnl/OMNLMirrorCoordinator.sol new file mode 100644 index 0000000..eb038c1 --- /dev/null +++ b/contracts/hybx-omnl/OMNLMirrorCoordinator.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IRouterClient} from "../ccip/IRouterClient.sol"; +import {ReserveCommitmentStore} from "./ReserveCommitmentStore.sol"; + +/** + * @title OMNLMirrorCoordinator + * @notice Primary-chain helper: commits to ReserveCommitmentStore then sends data-only CCIP to mirror receiver. + * @dev Grant RESERVE_COMMITTER_ROLE to this contract on ReserveCommitmentStore. + * Native fee: send `msg.value >= fee`. ERC-20 fee (e.g. LINK): approve this contract for `fee` before calling. + */ +contract OMNLMirrorCoordinator { + using SafeERC20 for IERC20; + + IRouterClient public immutable ccipRouter; + ReserveCommitmentStore public immutable reserveStore; + address public admin; + + uint64 public mirrorChainSelector; + address public mirrorReceiver; + address public feeToken; + + event MirrorChainConfigured(uint64 indexed selector, address indexed receiver, address feeToken); + event MirrorMessageSent(bytes32 indexed messageId, uint64 indexed destSelector, bytes32 indexed lineId, uint256 version); + + error OnlyAdmin(); + + modifier onlyAdmin() { + if (msg.sender != admin) revert OnlyAdmin(); + _; + } + + constructor(address router_, address reserveStore_, address admin_) { + require(router_ != address(0) && reserveStore_ != address(0) && admin_ != address(0), "OMNLMirrorCoordinator: zero"); + ccipRouter = IRouterClient(router_); + reserveStore = ReserveCommitmentStore(reserveStore_); + admin = admin_; + } + + function setMirrorDestination(uint64 selector, address receiver, address feeToken_) external onlyAdmin { + mirrorChainSelector = selector; + mirrorReceiver = receiver; + feeToken = feeToken_; + emit MirrorChainConfigured(selector, receiver, feeToken_); + } + + function transferAdmin(address next) external onlyAdmin { + require(next != address(0), "OMNLMirrorCoordinator: zero"); + admin = next; + } + + /// @notice Commit on primary then mirror state via CCIP (data-only message). + function commitReserveAndMirror( + bytes32 lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot + ) external payable returns (bytes32 messageId) { + require(mirrorReceiver != address(0) && mirrorChainSelector != 0, "OMNLMirrorCoordinator: mirror unset"); + reserveStore.commitReserve(lineId, R, validUntil, evidenceHash, merkleRoot); + ReserveCommitmentStore.Commitment memory c = reserveStore.getCommitment(lineId); + bytes memory data = abi.encode(c.version, lineId, R, validUntil, evidenceHash, merkleRoot); + + IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ + receiver: abi.encode(mirrorReceiver), + data: data, + tokenAmounts: new IRouterClient.TokenAmount[](0), + feeToken: feeToken, + extraArgs: "" + }); + + uint256 fee = ccipRouter.getFee(mirrorChainSelector, message); + if (feeToken == address(0)) { + require(msg.value >= fee, "OMNLMirrorCoordinator: fee"); + (messageId,) = ccipRouter.ccipSend{value: fee}(mirrorChainSelector, message); + } else { + IERC20 ft = IERC20(feeToken); + ft.safeTransferFrom(msg.sender, address(this), fee); + SafeERC20.safeIncreaseAllowance(ft, address(ccipRouter), fee); + (messageId,) = ccipRouter.ccipSend(mirrorChainSelector, message); + } + + emit MirrorMessageSent(messageId, mirrorChainSelector, lineId, c.version); + } + + function quoteMirrorFee( + bytes32 lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot + ) external view returns (uint256 fee) { + ReserveCommitmentStore.Commitment memory c = reserveStore.getCommitment(lineId); + uint256 nextVersion = c.version + 1; + bytes memory data = abi.encode(nextVersion, lineId, R, validUntil, evidenceHash, merkleRoot); + IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ + receiver: abi.encode(mirrorReceiver), + data: data, + tokenAmounts: new IRouterClient.TokenAmount[](0), + feeToken: feeToken, + extraArgs: "" + }); + return ccipRouter.getFee(mirrorChainSelector, message); + } +} diff --git a/contracts/hybx-omnl/OMNLMirrorReceiver.sol b/contracts/hybx-omnl/OMNLMirrorReceiver.sol new file mode 100644 index 0000000..518edd3 --- /dev/null +++ b/contracts/hybx-omnl/OMNLMirrorReceiver.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IRouterClient} from "../ccip/IRouterClient.sol"; +import {ReserveCommitmentStore} from "./ReserveCommitmentStore.sol"; + +/** + * @title OMNLMirrorReceiver + * @notice CCIP receiver on the mirror chain (e.g. 651940) applying mirrored reserve commits from primary (e.g. 138). + * @dev Router must be the canonical CCIP router; allowlist source selectors to prevent spoofed lanes. + */ +contract OMNLMirrorReceiver { + IRouterClient public immutable router; + ReserveCommitmentStore public immutable reserveStore; + address public admin; + + mapping(bytes32 => bool) public processedMessages; + mapping(uint64 => bool) public allowedSourceSelectors; + + event MirrorCommitReceived( + bytes32 indexed messageId, + uint64 indexed sourceChainSelector, + bytes32 indexed lineId, + uint256 version, + uint256 R, + bytes32 merkleRoot + ); + event SourceSelectorSet(uint64 indexed selector, bool allowed); + event AdminTransferred(address indexed previous, address indexed next); + + error OnlyRouter(); + error OnlyAdmin(); + error AlreadyProcessed(); + error SourceNotAllowed(); + + modifier onlyRouter() { + if (msg.sender != address(router)) revert OnlyRouter(); + _; + } + + modifier onlyAdmin() { + if (msg.sender != admin) revert OnlyAdmin(); + _; + } + + constructor(address router_, address reserveStore_, address admin_) { + require(router_ != address(0) && reserveStore_ != address(0) && admin_ != address(0), "OMNLMirrorReceiver: zero"); + router = IRouterClient(router_); + reserveStore = ReserveCommitmentStore(reserveStore_); + admin = admin_; + } + + function setSourceAllowed(uint64 sourceChainSelector, bool allowed) external onlyAdmin { + allowedSourceSelectors[sourceChainSelector] = allowed; + emit SourceSelectorSet(sourceChainSelector, allowed); + } + + function transferAdmin(address next) external onlyAdmin { + require(next != address(0), "OMNLMirrorReceiver: zero"); + emit AdminTransferred(admin, next); + admin = next; + } + + /** + * @notice CCIP entrypoint — payload abi.encode(version, lineId, R, validUntil, evidenceHash, merkleRoot) + */ + function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyRouter { + if (processedMessages[message.messageId]) revert AlreadyProcessed(); + if (!allowedSourceSelectors[message.sourceChainSelector]) revert SourceNotAllowed(); + + processedMessages[message.messageId] = true; + + (uint256 version, bytes32 lineId, uint256 R, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot) = + abi.decode(message.data, (uint256, bytes32, uint256, uint256, bytes32, bytes32)); + + reserveStore.applyMirrorCommit(lineId, R, validUntil, evidenceHash, merkleRoot, version); + + emit MirrorCommitReceived(message.messageId, message.sourceChainSelector, lineId, version, R, merkleRoot); + } +} diff --git a/contracts/hybx-omnl/PolicyMath.sol b/contracts/hybx-omnl/PolicyMath.sol new file mode 100644 index 0000000..8965002 --- /dev/null +++ b/contracts/hybx-omnl/PolicyMath.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title PolicyMath + * @notice Deterministic GRU M0/M1 rules: 1.2× reserves on M0; ≤5× M1 vs M0 per line. + * @dev Must match docs/hybx-omnl/HYBX_OMNL_POLICY_SPEC.md + */ +library PolicyMath { + uint256 internal constant M0_NUM = 12; + uint256 internal constant M0_DEN = 10; + uint256 internal constant M1_CAP_NUM = 5; + uint256 internal constant M1_CAP_DEN = 1; + + /// @notice Minimum reserves R required to back S0 units of M0 (ceil(1.2 * S0)). + function minReservesForM0(uint256 s0) internal pure returns (uint256) { + return Math.mulDiv(s0, M0_NUM, M0_DEN, Math.Rounding.Ceil); + } + + /// @notice Maximum M1 supply allowed for S0 units of M0 (5 * S0). + function maxM1ForM0(uint256 s0) internal pure returns (uint256) { + return Math.mulDiv(s0, M1_CAP_NUM, M1_CAP_DEN); + } + + function m0BackingOk(uint256 s0, uint256 r) internal pure returns (bool) { + return r >= minReservesForM0(s0); + } + + function m1ExpansionOk(uint256 s0, uint256 s1) internal pure returns (bool) { + return s1 <= maxM1ForM0(s0); + } + + function isCompliant(uint256 s0, uint256 s1, uint256 r) internal pure returns (bool) { + return m0BackingOk(s0, r) && m1ExpansionOk(s0, s1); + } + + function canMintM0(uint256 s0Before, uint256 mintAmount, uint256 r) internal pure returns (bool) { + unchecked { + uint256 s0After = s0Before + mintAmount; + return m0BackingOk(s0After, r); + } + } + + function canMintM1(uint256 s0, uint256 s1Before, uint256 mintAmount) internal pure returns (bool) { + unchecked { + uint256 s1After = s1Before + mintAmount; + return m1ExpansionOk(s0, s1After); + } + } +} diff --git a/contracts/hybx-omnl/ReserveCommitmentStore.sol b/contracts/hybx-omnl/ReserveCommitmentStore.sol new file mode 100644 index 0000000..c12cb93 --- /dev/null +++ b/contracts/hybx-omnl/ReserveCommitmentStore.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @title ReserveCommitmentStore + * @notice Stores attested reserve R per line with TTL, merkle evidence root, versioning, primary + mirror, + * and optional threshold ECDSA attestation for commits. + */ +contract ReserveCommitmentStore is AccessControl { + bytes32 public constant RESERVE_COMMITTER_ROLE = keccak256("RESERVE_COMMITTER_ROLE"); + bytes32 public constant ATTESTATION_ADMIN_ROLE = keccak256("ATTESTATION_ADMIN_ROLE"); + + /// @notice EIP-191 structured preimage prefix for attested commits (see commitReserveAttested). + bytes32 public constant ATTESTATION_TYPEHASH = + keccak256("OMNLReserveCommit(uint256 chainId,address store,bytes32 lineId,uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 nonce)"); + + struct Commitment { + uint256 R; + uint256 validUntil; + bytes32 evidenceHash; + bytes32 merkleRoot; + uint256 version; + } + + address public mirrorReceiver; + + mapping(bytes32 lineId => Commitment) private _commitments; + mapping(bytes32 lineId => uint256) public lineAttestationNonce; + + mapping(address => bool) public isAttestationSigner; + uint256 public attestationThreshold; + + event ReserveCommitted( + bytes32 indexed lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot, + uint256 version, + address indexed by + ); + event MirrorReceiverUpdated(address indexed oldReceiver, address indexed newReceiver); + event AttestationSignersUpdated(uint256 threshold); + event AttestationSignerSet(address indexed signer, bool active); + + constructor(address admin) { + require(admin != address(0), "ReserveCommitmentStore: zero admin"); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(RESERVE_COMMITTER_ROLE, admin); + _grantRole(ATTESTATION_ADMIN_ROLE, admin); + } + + function setMirrorReceiver(address receiver) external onlyRole(DEFAULT_ADMIN_ROLE) { + address old = mirrorReceiver; + mirrorReceiver = receiver; + emit MirrorReceiverUpdated(old, receiver); + } + + function setAttestationSigner(address signer, bool active) external onlyRole(ATTESTATION_ADMIN_ROLE) { + isAttestationSigner[signer] = active; + emit AttestationSignerSet(signer, active); + } + + function setAttestationThreshold(uint256 threshold) external onlyRole(ATTESTATION_ADMIN_ROLE) { + attestationThreshold = threshold; + emit AttestationSignersUpdated(threshold); + } + + /// @notice Primary chain (or designated committer) updates reserves. + function commitReserve( + bytes32 lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot + ) external onlyRole(RESERVE_COMMITTER_ROLE) { + _commit(lineId, R, validUntil, evidenceHash, merkleRoot, msg.sender); + } + + /// @notice Threshold ECDSA attestation (any EOA may submit once enough distinct signers included). + function commitReserveAttested( + bytes32 lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot, + uint256 nonce, + bytes[] calldata signatures + ) external { + require(attestationThreshold > 0, "ReserveCommitmentStore: attestation off"); + require(nonce == lineAttestationNonce[lineId], "ReserveCommitmentStore: nonce"); + + bytes32 digest = keccak256( + abi.encode( + ATTESTATION_TYPEHASH, + block.chainid, + address(this), + lineId, + R, + validUntil, + evidenceHash, + merkleRoot, + nonce + ) + ); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); + + uint256 n = signatures.length; + require(n >= attestationThreshold, "ReserveCommitmentStore: sig count"); + address[] memory seen = new address[](n); + uint256 uniq = 0; + + for (uint256 i = 0; i < n; i++) { + address recovered = ECDSA.recover(ethSignedMessageHash, signatures[i]); + require(isAttestationSigner[recovered], "ReserveCommitmentStore: not signer"); + for (uint256 j = 0; j < uniq; j++) { + require(seen[j] != recovered, "ReserveCommitmentStore: duplicate signer"); + } + seen[uniq] = recovered; + unchecked { + ++uniq; + } + } + require(uniq >= attestationThreshold, "ReserveCommitmentStore: threshold"); + + lineAttestationNonce[lineId] = nonce + 1; + _commit(lineId, R, validUntil, evidenceHash, merkleRoot, msg.sender); + } + + function _commit( + bytes32 lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot, + address by + ) internal { + Commitment storage c = _commitments[lineId]; + unchecked { + c.version += 1; + } + c.R = R; + c.validUntil = validUntil; + c.evidenceHash = evidenceHash; + c.merkleRoot = merkleRoot; + emit ReserveCommitted(lineId, R, validUntil, evidenceHash, merkleRoot, c.version, by); + } + + /// @notice Applied by OMNLMirrorReceiver after CCIP validation (monotonic version). + function applyMirrorCommit( + bytes32 lineId, + uint256 R, + uint256 validUntil, + bytes32 evidenceHash, + bytes32 merkleRoot, + uint256 version + ) external { + require(msg.sender == mirrorReceiver, "ReserveCommitmentStore: only mirror"); + Commitment storage c = _commitments[lineId]; + require(version > c.version, "ReserveCommitmentStore: version"); + c.version = version; + c.R = R; + c.validUntil = validUntil; + c.evidenceHash = evidenceHash; + c.merkleRoot = merkleRoot; + emit ReserveCommitted(lineId, R, validUntil, evidenceHash, merkleRoot, version, msg.sender); + } + + function getCommitment(bytes32 lineId) external view returns (Commitment memory) { + return _commitments[lineId]; + } +} diff --git a/contracts/hybx-omnl/interfaces/IZkReserveProofVerifier.sol b/contracts/hybx-omnl/interfaces/IZkReserveProofVerifier.sol new file mode 100644 index 0000000..fe38428 --- /dev/null +++ b/contracts/hybx-omnl/interfaces/IZkReserveProofVerifier.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IZkReserveProofVerifier + * @notice Optional hook for zk-SNARK verification that R >= f(S0) with private bank balances. + * @dev Deploy a verifier implementation and call from an extended ReserveCommitmentStore or wrapper. + */ +interface IZkReserveProofVerifier { + /// @notice Returns true if the proof is valid for the given public inputs (e.g. commitment_R, lineId hash). + function verifyProof(bytes calldata proof, uint256[] calldata publicInputs) external view returns (bool valid); +} diff --git a/docs/hybx-omnl/CCIP_MIRROR_FLOW.md b/docs/hybx-omnl/CCIP_MIRROR_FLOW.md new file mode 100644 index 0000000..4d7e222 --- /dev/null +++ b/docs/hybx-omnl/CCIP_MIRROR_FLOW.md @@ -0,0 +1,41 @@ +# CCIP mirror flow — Chain 138 ↔ ALL Mainnet (651940) + +## Roles + +- **Primary reserve writer:** Chain **138** (`ReserveCommitmentStore.commitReserve` or `commitReserveAttested`) after operational attestation signing. +- **One-shot mirror send:** `OMNLMirrorCoordinator.commitReserveAndMirror` on **138** commits then sends CCIP (native fee). Grant `RESERVE_COMMITTER_ROLE` on `ReserveCommitmentStore` to the coordinator address. +- **Mirror chain:** **651940** receives identical commitments via `OMNLMirrorReceiver.ccipReceive`, which calls `ReserveCommitmentStore.applyMirrorCommit` (caller must equal `mirrorReceiver` set on the store). + +## Finality + +- **138 (QBFT):** treat blocks as final immediately for attestation pipeline (`confirmations: 1` in indexer config). +- **651940:** wait **12 confirmations** before treating mirrored state as binding for **breaker automation** that affects user funds (matches [chains.ts](../../services/token-aggregation/src/config/chains.ts) defaults). + +## Payload encoding + +Off-chain helper (repo): `services/token-aggregation/scripts/encode-omnl-mirror-payload.mjs`. + +`OMNLMirrorReceiver` decodes `message.data` as: + +```text +abi.encode(version, lineId, R, validUntil, evidenceHash, merkleRoot) +``` + +- `version` must be **strictly greater** than the stored version for `lineId` on the mirror store. +- `lineId` is `bytes32`. +- `R`, `validUntil`, `evidenceHash`, `merkleRoot` must match the primary commit being mirrored. + +## Router integration + +1. Deploy `OMNLMirrorReceiver(router651940, reserveStore651940, admin)` on ALL Mainnet. +2. On `ReserveCommitmentStore` (651940): `setMirrorReceiver(receiverAddr)` (admin). +3. On receiver: `setSourceAllowed(chainSelector138, true)` with the **Chainlink CCIP selector** for Chain 138 as **source**. +4. From Chain 138, send CCIP messages to `receiver` on 651940 with **no token amounts** (data-only message) using the encoded payload above. Fee in native or LINK per router config. + +## Replay protection + +`OMNLMirrorReceiver` marks `messageId` processed (same pattern as [CCIPRelayBridge](../../contracts/relay/CCIPRelayBridge.sol)). + +## Desync breaker + +If mirrored `version` / `R` on 651940 lags primary beyond `N` blocks (operator config), automation documented in [RUNBOOK_CIRCUIT_BREAKERS.md](./RUNBOOK_CIRCUIT_BREAKERS.md) should alert and optionally invoke `OMNLCircuitBreaker.setLinePaused` / `setGlobalPaused`. diff --git a/docs/hybx-omnl/DEPLOYMENT_CHECKLIST.md b/docs/hybx-omnl/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..371af44 --- /dev/null +++ b/docs/hybx-omnl/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,24 @@ +# HYBX OMNL — deployment checklist + +Use with `config/deployment-omnl.example.env` (copy to `.env` on the host; never commit secrets). + +## Pre-flight + +1. **Contracts**: `forge script script/hybx-omnl/DeployOMNLStack.s.sol` (or `DeployMirrorCoordinator.s.sol` for coordinator-only) on target chains; record addresses. +2. **CCIP**: Router and LINK (or native fee) match `hybx-omnl-cross-chain-lines.json`; coordinator `approve` path for ERC-20 fee token if used. +3. **Policy**: `hybx-omnl-policy.json` matches on-chain `PolicyMath` / registry limits (1.2× M0, 5× M1). +4. **Fineract**: GL accounts exist; pagination env `OMNL_FINERACT_GL_PAGE_LIMIT` set if >200 accounts. + +## Token aggregation service + +1. Set `OMNL_*`, `ENABLE_OMNL_EVENT_POLLER`, `OMNL_POLLER_STATE_PATH` (optional), webhook URLs/secrets, optional `OMNL_API_KEY` / `OMNL_DASHBOARD_TOKEN`, `OMNL_RATE_LIMIT_*` as needed. +2. `pnpm run build` in `services/token-aggregation`. +3. Health: `GET /health`; OMNL: `GET /api/v1/omnl/compliance`; dashboard: `GET /omnl/dashboard`. +4. Run `bash scripts/hybx-omnl/verify-deployment.sh` (Forge `hybx-omnl` tests + `tsc` in `services/token-aggregation`). +5. **Publish snapshot** (`smom-dbis-138-publish/`): run the same script name there for TypeScript + `omnl-reconcile-report.mjs` only; Forge deps live in the main repo. + +## Post-deploy + +1. Anchor reconciliation hash: `pnpm run omnl:reconcile:artifact` (repo root) or `bash scripts/hybx-omnl/omnl-reconcile-artifact.sh` (writes `artifacts/omnl-reconcile/omnl-reconcile-sha256.txt`) or `pnpm run omnl:reconcile` under `services/token-aggregation` / `node services/token-aggregation/scripts/omnl-reconcile-report.mjs` → store `sha256` in ops log. Cron and GitHub Actions: see `docs/hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md`. +2. Mirror smoke: send test payload per [CCIP_MIRROR_FLOW.md](CCIP_MIRROR_FLOW.md) (staging first). +3. Operational compliance: see `OPERATIONAL_COMPLIANCE.md` (retention, webhook signing, break-glass). diff --git a/docs/hybx-omnl/EXTERNAL_AUDIT_CHECKLIST.md b/docs/hybx-omnl/EXTERNAL_AUDIT_CHECKLIST.md new file mode 100644 index 0000000..a45a8d8 --- /dev/null +++ b/docs/hybx-omnl/EXTERNAL_AUDIT_CHECKLIST.md @@ -0,0 +1,31 @@ +# HYBX OMNL — external audit checklist + +Use this with a third-party firm before high-value production. Scope aligns with [SECURITY_THREAT_MODEL.md](SECURITY_THREAT_MODEL.md). + +## Solidity (in scope) + +- [ ] `PolicyMath.sol` — rounding, overflow, parameter bounds vs documented policy. +- [ ] `InstrumentRegistry.sol` — role changes, line lifecycle, token registration assumptions. +- [ ] `ReserveCommitmentStore.sol` — `commitReserve` / `commitReserveAttested`, ECDSA digest, replay, threshold logic. +- [ ] `ComplianceCore.sol` — `getCompliance` semantics vs `PolicyMath`, stale attestation, `reportingCompliant`. +- [ ] `OMNLCircuitBreaker.sol` — pause semantics, admin roles. +- [ ] `OMNLMirrorReceiver.sol` — CCIP payload decoding, selector allowlist, monotonic version. +- [ ] `OMNLMirrorCoordinator.sol` — native vs ERC-20 fee path, `approve`/`SafeERC20`, reentrancy surface (minimal). + +## Operational evidence to provide auditors + +- [ ] Deployed addresses per chain (138 / 651940) and verification on block explorers. +- [ ] Key ceremony summary (HSM / multisig); no plaintext prod keys in CI. +- [ ] CCIP lane configuration (router, selectors, fee token). +- [ ] Sample `ReserveCommitted` and mirror receive transactions on testnet/staging. + +## Off-chain (optional scope) + +- [ ] Token-aggregation OMNL routes — rate limits, `OMNL_API_KEY` usage, webhook HMAC verification at receivers. +- [ ] IPSAS registry / journal matrix change control (who can commit, how hash is anchored). + +## Sign-off + +| Finding | Severity | Remediation | Retest date | +|---------|----------|-------------|-------------| +| | | | | diff --git a/docs/hybx-omnl/HYBX_OMNL_POLICY_SPEC.md b/docs/hybx-omnl/HYBX_OMNL_POLICY_SPEC.md new file mode 100644 index 0000000..dcad99d --- /dev/null +++ b/docs/hybx-omnl/HYBX_OMNL_POLICY_SPEC.md @@ -0,0 +1,54 @@ +# HYBX OMNL — frozen policy specification + +This document locks **deterministic** monetary rules for GRU v2 (ISO-4217 lines) and **cXAU\*** (troy ounce) instruments. On-chain code in `contracts/hybx-omnl/` must match this spec. + +## Scope + +- **Per-line aggregates:** Each `lineId` (e.g. USD-stable line, EUR line, XAU line) has independent `S0`, `S1`, `R`. The **5:1 M1 cap** applies **per line**, not globally across unrelated ISO codes. +- **Units:** All of `S0`, `S1`, `R` for a given `lineId` use the **same minor unit** as the registered M0 token (e.g. 6 decimals for typical c\* stables). For XAU lines, **1.0 token = 1 troy ounce** at full precision (see explorer cross-check); reserves `R` are **troy ounces** in that same atom. + +## Rounding (mulDiv) + +Let `M0_NUM = 12`, `M0_DEN = 10` (1.2× backing). + +- **Minimum reserves** required for current M0 supply `S0`: + + `minR(S0) = ceil(S0 * M0_NUM / M0_DEN)` using `Math.mulDiv(S0, M0_NUM, M0_DEN, Rounding.Ceil)`. + + Intuition: any fractional atom of M0 requires full ceil so backing never falls below 1.2× in discrete units. + +- **M1 cap** for current `S0`: + + `maxS1(S0) = S0 * 5` (exact; no rounding down of the cap in favor of more M1). + +Compliance: + +- **M0 backing:** `R >= minR(S0)`. +- **M1 expansion:** `S1 <= maxS1(S0)`. + +**Mint simulation:** Before minting `Δ` M0: require `R >= minR(S0 + Δ)`. Before minting `Δ` M1: require `S1 + Δ <= maxS1(S0)`. + +## Instrument registration + +- Each **line** registers exactly one **M0** ERC-20 and one **M1** ERC-20 (`InstrumentRegistry`). +- Tokens used for supply reads must be **standard** `ERC20.totalSupply()`; rebasing or fee-on-transfer tokens are **out of scope** unless wrapped to a fixed-supply view. +- **ISO-4217** numeric code is stored for GRU v2 fiat lines; XAU lines set `isXAU = true` and ignore ISO fiat code. + +## Attestation vs price oracles + +- **Reserve commitments `R`** come only from `ReserveCommitmentStore` (manual multisig / HSM pipeline). They are **not** PMM or CoinGecko prices. +- **Price / PMM oracles** remain separate ([ORACLE_AND_KEEPER_CHAIN138](../integration/ORACLE_AND_KEEPER_CHAIN138.md)). + +## Primary chain for writes + +- **Default:** Chain **138** is the **primary** committer for `R` and policy parameter updates; ALL Mainnet (**651940**) receives **mirrored** commitments via CCIP (`OMNLMirrorReceiver`). Operators may switch primary only via governance-controlled addresses documented at deploy time. + +## Stale attestation (operational default) + +- If `now > validUntil` for a line’s last commitment, the **API** surfaces `attestationStale: true`. On-chain **pause** of M1 mint is **optional** and controlled by `OMNLCircuitBreaker` TTL — default **warn-only** until `enforceStaleBlockM0/M1` is enabled. +- **`reportingCompliant` (on-chain / API):** `policyOk && !attestationStale && operational` — use this for dashboards when stale attestations must fail the “green” status even if pure math (`policyOk`) still holds. +- **Merkle evidence:** `ReserveCommitmentStore` stores `merkleRoot` alongside `evidenceHash` for periodic reconciliation roots. + +## Cross-chain aggregated supply (API) + +- For lines with M0/M1 on both 138 and 651940, configure [`config/hybx-omnl-cross-chain-lines.json`](../../config/hybx-omnl-cross-chain-lines.json) and call **`GET /api/v1/omnl/compliance-aggregated/:lineId`**. Reserves `R` / TTL are taken from primary (**138**) `ComplianceCore`; supplies are summed per `totalSupply()` across chains. diff --git a/docs/hybx-omnl/OMNL_IPSAS_API.md b/docs/hybx-omnl/OMNL_IPSAS_API.md new file mode 100644 index 0000000..71c4a42 --- /dev/null +++ b/docs/hybx-omnl/OMNL_IPSAS_API.md @@ -0,0 +1,67 @@ +# OMNL IPSAS / GL validation API (token-aggregation) + +Endpoints are served under **`/api/v1`** when the token-aggregation service is running. They tie **on-chain OMNL compliance** to **IPSAS-aligned GL codes** documented for OMNL Hybx (Fineract). + +**Discovery:** `GET /api/v1/omnl/catalog` — machine-readable list of all OMNL routes, auth notes, and query parameters. + +**OpenAPI 3:** `GET /api/v1/omnl/openapi.json` — static OpenAPI 3.0 document for Swagger UI, Postman import, and codegen. + +## Configuration files (smom-dbis-138) + +| File | Purpose | +|------|---------| +| [`config/omnl-ipsas-gl-registry.json`](../../config/omnl-ipsas-gl-registry.json) | Canonical GL codes (1000, 1050, 2000, 2100, 3000), IPSAS references, allowed pairs, monetary-layer hints | +| [`config/omnl-journal-matrix.json`](../../config/omnl-journal-matrix.json) | T-001…T-008 journal lines | + +Override paths with `OMNL_IPSAS_GL_REGISTRY` and `OMNL_JOURNAL_MATRIX_PATH` if needed. + +## Fineract (live compare & health) + +Set: + +- `OMNL_FINERACT_BASE_URL` +- `OMNL_FINERACT_TENANT` (default `omnl`) +- `OMNL_FINERACT_USER` (default `app.omnl`) — or legacy **`OMNL_FINERACT_USERNAME`** +- `OMNL_FINERACT_PASSWORD` +- `OMNL_FINERACT_GL_PAGE_LIMIT` (pagination for large tenants) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/omnl/ipsas/fineract-health` | Probe `/glaccounts?limit=1` — returns `ok`, `statusCode`, `configured` | +| GET | `/omnl/ipsas/fineract-compare` | Full GL list vs registry (requires **`OMNL_API_KEY`** when that env is set) | + +## IPSAS validation + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/omnl/ipsas/registry` | Full IPSAS GL registry JSON | +| GET | `/omnl/ipsas/matrix` | Journal matrix entries | +| GET | `/omnl/ipsas/validate-pair?debitGlCode=&creditGlCode=` | Validate one pair (registry + matrix) | +| POST | `/omnl/ipsas/validate-pairs` | Body: `{ "pairs": [{ "debitGlCode", "creditGlCode" }, ...] }` — batch validation | +| GET | `/omnl/ipsas/layer/:layer` | `m0_reserve` \| `m1_liability` \| `settlement` \| `equity` | +| GET | `/omnl/ipsas/compliance-context/:lineId?aggregated=1` | Compliance snapshot + IPSAS guidance (**`OMNL_API_KEY`** when set) | + +## Cross-cutting OMNL APIs (same `/api/v1` prefix) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/omnl/reconcile-anchor` | SHA-256 of canonical IPSAS registry + journal matrix (same as `omnl-reconcile-report.mjs`) | +| GET | `/omnl/integration-status` | Booleans for configured env groups (no secrets) | +| GET | `/omnl/cross-chain-lines` | `hybx-omnl-cross-chain-lines.json` content + path | +| GET | `/omnl/zk-verifier` | Address from `OMNL_ZK_VERIFIER` | +| GET | `/omnl/mirror-coordinator?chainId=138` | On-chain `mirrorChainSelector`, `mirrorReceiver`, `feeToken` | + +## Example + +```http +GET /api/v1/omnl/ipsas/validate-pair?debitGlCode=1050&creditGlCode=2000 +``` + +```http +POST /api/v1/omnl/ipsas/validate-pairs +Content-Type: application/json + +{"pairs":[{"debitGlCode":"1050","creditGlCode":"2000"}]} +``` + +Expect `ipsasCompliantPair: true` when the pair is allowed in the registry or matrix. diff --git a/docs/hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md b/docs/hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md new file mode 100644 index 0000000..04b0720 --- /dev/null +++ b/docs/hybx-omnl/OMNL_RECONCILE_CRON_AND_CI.md @@ -0,0 +1,55 @@ +# OMNL reconcile — cron and CI + +The anchor is the SHA-256 of canonical JSON built from `config/omnl-ipsas-gl-registry.json` and `config/omnl-journal-matrix.json` (see `services/token-aggregation/scripts/omnl-reconcile-report.mjs`). + +## One-off (repo root) + +```bash +bash scripts/hybx-omnl/omnl-reconcile-artifact.sh +``` + +Outputs under `artifacts/omnl-reconcile/` (gitignored locally): + +| File | Purpose | +|------|---------| +| `omnl-reconcile-.json` | Immutable run record | +| `omnl-reconcile-latest.json` | Copy of last run | +| `omnl-reconcile-sha256.txt` | Single line: hex digest for scripts / alerting | +| `omnl-reconcile-ci-meta.json` | Present when `GITHUB_*` env vars are set | + +Override output directory: `OMNL_RECONCILE_ARTIFACT_DIR=/var/lib/omnl bash scripts/hybx-omnl/omnl-reconcile-artifact.sh` + +## Cron example + +Run daily at 06:00 UTC. The script calls `node` on `omnl-reconcile-report.mjs` (no `npm install` required; only Node.js and repo files). + +```cron +# OMNL IPSAS / journal matrix anchor (adjust paths and log location) +0 6 * * * cd /opt/smom-dbis-138 && /usr/bin/bash scripts/hybx-omnl/omnl-reconcile-artifact.sh >> /var/log/omnl-reconcile.log 2>&1 +``` + +After registry or matrix edits in git, re-run the script (or rely on CI) and archive the new `sha256` in your ops log. + +## GitHub Actions + +### Scheduled / manual anchor + +Workflow: `.github/workflows/omnl-reconcile.yml` + +- **schedule**: weekly (edit cron as needed) +- **workflow_dispatch**: manual run +- **push** / **pull_request**: when OMNL config or the reconcile script changes + +Artifacts: download **omnl-reconcile-\** from the Actions run; it contains the same files as above. + +### PR checks (TypeScript + anchor, no Forge) + +Workflow: `.github/workflows/hybx-omnl-ts.yml` + +- **workflow_dispatch**: run manually from the Actions tab (no path filter). +- Runs on **pull_request** and **push** when OMNL contracts, token-aggregation, OMNL config, or `scripts/hybx-omnl/**` change. +- Steps: `npm ci` in `services/token-aggregation`, `omnl-reconcile-artifact.sh`, `npm run build`, uploads **`artifacts/omnl-reconcile/`** (14-day retention). + +Full stack validation (Forge `hybx-omnl` + `tsc`): `pnpm run omnl:verify` or `bash scripts/hybx-omnl/ci-omnl-validation.sh` locally (requires Foundry + `lib/`). + +Both OMNL workflows use **concurrency** (cancel in-progress on the same ref) to avoid stacked runs on rapid pushes. diff --git a/docs/hybx-omnl/OPERATIONAL_COMPLIANCE.md b/docs/hybx-omnl/OPERATIONAL_COMPLIANCE.md new file mode 100644 index 0000000..51f0275 --- /dev/null +++ b/docs/hybx-omnl/OPERATIONAL_COMPLIANCE.md @@ -0,0 +1,45 @@ +# Operational compliance (OMNL) + +## Webhooks + +- Configure `OMNL_WEBHOOK_URLS` (comma-separated HTTPS endpoints). +- Set `OMNL_WEBHOOK_SECRET`. Each POST body is **UTF-8 JSON**; **`X-OMNL-Signature`** = `sha256=` + **hex(HMAC-SHA256(secret, rawBody))** (same bytes as the request body). Use `verifyOmnlWebhookSignature()` from `omnl-webhooks.ts` or reimplement with the same algorithm. **Timing-safe** compare the full header value. +- Payloads include **`deliveryId`** (e.g. `138-12345-2`) for idempotent processing at the receiver. +- Prefer allowlists and TLS 1.2+ only; rotate secrets on break-glass. + +## API hardening + +- **`OMNL_API_KEY`**: when set, `GET /api/v1/omnl/ipsas/fineract-compare` and `.../compliance-context/:lineId` require `Authorization: Bearer ` or `?access_token=`. +- **`OMNL_DASHBOARD_TOKEN`**: when set, `GET /omnl/dashboard` requires the same token via `?access_token=` or header `X-OMNL-Dashboard-Token`. For Fineract compare in the embedded page, open **`/omnl/dashboard?access_token=`** so the script can call protected routes. +- **OMNL rate limit**: `OMNL_RATE_LIMIT_MAX` / `OMNL_RATE_LIMIT_WINDOW_MS` (default 30/min per IP on `/api/v1/omnl/*`, in addition to the global API limiter). + +## Logs and retention + +- Reserve commit and compliance events are emitted to application logs; align retention with your policy (often 90 days minimum for financial audit support). +- **Config anchor (IPSAS / journal matrix):** after registry/matrix JSON changes, run `bash scripts/hybx-omnl/omnl-reconcile-artifact.sh` — this hashes **off-chain config files only**, not custodian bank balances vs on-chain `R`. Use a separate control for **bank ↔ on-chain** reconciliation if required for your auditor. +- Automate anchors via cron or GitHub Actions — [OMNL_RECONCILE_CRON_AND_CI.md](OMNL_RECONCILE_CRON_AND_CI.md). + +## Poller state + +- **`OMNL_POLLER_STATE_PATH`**: optional path for JSON storing last processed block per chain (default: `.omnl-poller-state.json` in `cwd`). Survives restarts to avoid re-querying large ranges; webhook consumers should still treat **`deliveryId`** as idempotent. + +## Recommended alerts (operational) + +Wire your log/metrics stack to alert on: + +- Webhook POST failures (warn logs from `omnl-webhooks.ts`). +- `reportingCompliant === false` or `attestationStale` from compliance APIs for critical `lineId`s. +- `mirror-status` / `inSync === false` when both reserve stores are configured. +- Repeated `401` on OMNL routes (possible credential scanning). + +## Break-glass + +- Document who can pause `OMNLCircuitBreaker` and rotate coordinator keys; store procedures outside this repo per org policy. + +## Data minimization + +- Webhook payloads avoid full PII; line IDs and hashes only unless contractually required. + +## External audit + +- See [EXTERNAL_AUDIT_CHECKLIST.md](EXTERNAL_AUDIT_CHECKLIST.md). diff --git a/docs/hybx-omnl/README.md b/docs/hybx-omnl/README.md new file mode 100644 index 0000000..4fc5047 --- /dev/null +++ b/docs/hybx-omnl/README.md @@ -0,0 +1,24 @@ +# HYBX OMNL documentation + +Multi-chain instrument registry, reserve commitments, compliance core, IPSAS-aligned GL reporting, and optional CCIP mirror to ALL Mainnet (651940). + +| Document | Purpose | +|----------|---------| +| [HYBX_OMNL_POLICY_SPEC.md](HYBX_OMNL_POLICY_SPEC.md) | On-chain policy math (1.2× M0, 5× M1) and limits | +| [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) | Contracts, env, API, verification | +| [CCIP_MIRROR_FLOW.md](CCIP_MIRROR_FLOW.md) | Primary → mirror CCIP payload and roles | +| [OMNL_IPSAS_API.md](OMNL_IPSAS_API.md) | REST routes for compliance + IPSAS / Fineract GL | +| [OMNL_RECONCILE_CRON_AND_CI.md](OMNL_RECONCILE_CRON_AND_CI.md) | Anchor hash: cron, `omnl-reconcile-artifact.sh`, CI artifacts | +| [EXTERNAL_AUDIT_CHECKLIST.md](EXTERNAL_AUDIT_CHECKLIST.md) | Third-party audit scope checklist | +| [OPERATIONAL_COMPLIANCE.md](OPERATIONAL_COMPLIANCE.md) | Webhooks, logs, break-glass, data minimization | +| [RUNBOOK_CIRCUIT_BREAKERS.md](RUNBOOK_CIRCUIT_BREAKERS.md) | Circuit breaker operations | +| [SECURITY_THREAT_MODEL.md](SECURITY_THREAT_MODEL.md) | Threat notes | +| [ZK_INTEGRATION.md](ZK_INTEGRATION.md) | Optional ZK reserve proof verifier hook | + +Configuration: `config/hybx-omnl-policy.json`, `config/hybx-omnl-cross-chain-lines.json`, `config/omnl-ipsas-gl-registry.json`, `config/omnl-journal-matrix.json`, `config/deployment-omnl.example.env`. + +Contracts: `contracts/hybx-omnl/`. Tests: `pnpm run forge:test:omnl` or `bash scripts/forge/scope.sh test hybx-omnl`. Full check: `pnpm run omnl:verify` (repo root). Anchor files: `pnpm run omnl:reconcile:artifact`. + +GitHub Actions: `.github/workflows/hybx-omnl-ts.yml` (TypeScript build + anchor on PR/push), `.github/workflows/omnl-reconcile.yml` (weekly anchor + CI meta); both support **workflow_dispatch**. Publish snapshot `smom-dbis-138-publish/` uses the same workflows; Forge-heavy checks run in the main repo only (`pnpm run omnl:verify`). + +Scripts: `bash scripts/hybx-omnl/sync-to-publish.sh` (mirror OMNL paths to publish tree), `node scripts/hybx-omnl/validate-cross-chain-config.mjs` (JSON shape + addresses for `hybx-omnl-cross-chain-lines.json`). diff --git a/docs/hybx-omnl/RUNBOOK_CIRCUIT_BREAKERS.md b/docs/hybx-omnl/RUNBOOK_CIRCUIT_BREAKERS.md new file mode 100644 index 0000000..863c4d2 --- /dev/null +++ b/docs/hybx-omnl/RUNBOOK_CIRCUIT_BREAKERS.md @@ -0,0 +1,30 @@ +# HYBX OMNL — circuit breakers and operations + +## Controls (on-chain) + +| Control | Contract | When to use | +|---------|----------|-------------| +| Global pause | `OMNLCircuitBreaker.setGlobalPaused(true)` | Bridge desync, key compromise affecting multiple lines, emergency | +| Line pause | `OMNLCircuitBreaker.setLinePaused(lineId, true)` | Single-asset stress, custodian dispute | +| Stale enforcement | `OMNLCircuitBreaker.setStaleEnforcement(blockM0, blockM1)` | Turn on hard **revert** on mint when `validUntil` passed (default warn-only via API) | + +Roles: `GUARDIAN_ROLE` (global), `PAUSER_ROLE` (line), `DEFAULT_ADMIN_ROLE` (stale policy). + +## Signals + +1. **TTL monitor** — `services/token-aggregation/scripts/omnl-ttl-monitor.mjs` (cron every 1–5 min). Exit 1 → page on-call. +2. **Cross-chain drift** — `GET /api/v1/omnl/mirror-status/:lineId` compares reserve **version** and `r` (requires `OMNL_RESERVE_STORE_138` and `OMNL_RESERVE_STORE_651940`). `GET /api/v1/omnl/health` (needs `OMNL_HEALTH_LINE_ID`) compares compliance snapshots including `reportingCompliant`. +3. **Policy breach** — index `ComplianceSnapshot` events or poll `getCompliance` for `policyOk: false`. + +## Runbook: mirror lag + +1. Check CCIP message queue / relayer health on primary. +2. Verify `allowedSourceSelectors` on `OMNLMirrorReceiver` (651940). +3. Compare `version` in `ReserveCommitted` events 138 vs 651940. +4. If unrecoverable within SLA: `setGlobalPaused(true)`, investigate, then unpause after replay or manual `commitReserve` alignment. + +## Runbook: stale attestation + +1. Run custodian pipeline; submit new `commitReserve` on primary with fresh `validUntil`. +2. Mirror to 651940 via CCIP per [CCIP_MIRROR_FLOW.md](./CCIP_MIRROR_FLOW.md). +3. If still stale after SLA: pause M1 mints (`setLinePause` or `setStaleEnforcement`). diff --git a/docs/hybx-omnl/SECURITY_THREAT_MODEL.md b/docs/hybx-omnl/SECURITY_THREAT_MODEL.md new file mode 100644 index 0000000..81919be --- /dev/null +++ b/docs/hybx-omnl/SECURITY_THREAT_MODEL.md @@ -0,0 +1,30 @@ +# HYBX OMNL — threat model and audit scope + +## Trust boundaries + +- **On-chain policy** (`PolicyMath`, `ComplianceCore`) is deterministic given inputs `(S0, S1, R)` from `IERC20.totalSupply` and `ReserveCommitmentStore`. +- **Economic truth of R** depends on **custodian attestation** and operational security of signing keys — not on PMM or spot price oracles. + +## Threats and mitigations + +| Threat | Impact | Mitigation | +|--------|--------|------------| +| Compromised `RESERVE_COMMITTER` key | Fake reserves | Multisig / HSM, key rotation, monitoring on `ReserveCommitted` | +| Compromised attestation signer set (`commitReserveAttested`) | Forged threshold commits | Rotate `isAttestationSigner`, raise `attestationThreshold`, monitor `lineAttestationNonce` | +| Compromised CCIP lane | Wrong mirror updates | `allowedSourceSelectors`, replay protection on `messageId` | +| Malicious ERC-20 (inflated totalSupply) | False compliance | Register only audited tokens; avoid rebasing tokens without adapter | +| Governance capture | Parameter / role theft | Timelock, split roles (`GUARDIAN`, `PAUSER`), multi-sig admin | +| API layer spoofing | Misleading dashboard | API reads **only** from published `ComplianceCore` addresses; publish ABIs | + +## External audit scope (recommended) + +1. `contracts/hybx-omnl/*.sol` — policy rounding, reentrancy (minimal external calls), access control. +2. `OMNLMirrorReceiver` — decoding, version monotonicity, selector allowlist. +3. Operational: HSM usage for `commitReserve`, CCIP operational runbooks. + +Structured checklist: [EXTERNAL_AUDIT_CHECKLIST.md](EXTERNAL_AUDIT_CHECKLIST.md). + +## Key ceremony (outline) + +- Generate `RESERVE_COMMITTER` / admin keys in HSM; no plaintext long-lived prod keys in CI. +- Document signer roster on-chain (`bytes32` merkle root optional extension) for auditor parity. diff --git a/docs/hybx-omnl/ZK_INTEGRATION.md b/docs/hybx-omnl/ZK_INTEGRATION.md new file mode 100644 index 0000000..af45ed5 --- /dev/null +++ b/docs/hybx-omnl/ZK_INTEGRATION.md @@ -0,0 +1,9 @@ +# ZK reserve attestation (optional extension) + +On-chain policy uses public inputs `(S0, S1, R)` from `ERC20.totalSupply` and `ReserveCommitmentStore`. For **privacy-preserving** proof that backing exists without disclosing raw bank balances: + +1. Implement a **verifier contract** conforming to [`IZkReserveProofVerifier.sol`](../../contracts/hybx-omnl/interfaces/IZkReserveProofVerifier.sol). +2. Off-chain: generate proofs with your chosen circuit (public inputs: commitment to R, line id, snapshot block). +3. Wire: either wrap `commitReserve` with a step that checks `verifyProof` before accepting R, or post proofs alongside `evidenceHash` / `merkleRoot` for auditor tooling only. + +This repo does not ship a production circuit or verifier bytecode; the interface is the integration boundary for audits. diff --git a/foundry.toml b/foundry.toml index ac2ccfd..9067f2e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -70,6 +70,13 @@ via_ir = true # Cronos chain 25 currently needs pre-Shanghai bytecode; target Paris to avoid PUSH0. evm_version = "paris" +[profile.chain138_legacy] +optimizer = true +optimizer_runs = 100 +via_ir = true +# Chain 138 currently rejects PUSH0 bytecode as well; target Paris for gas-canonical deployment flows. +evm_version = "paris" + # RPC endpoints — use: forge create ... --rpc-url chain138 # Prevents default localhost:8545 when ETH_RPC_URL not set [rpc_endpoints] diff --git a/script/hybx-omnl/DeployMirrorCoordinator.s.sol b/script/hybx-omnl/DeployMirrorCoordinator.s.sol new file mode 100644 index 0000000..8a52751 --- /dev/null +++ b/script/hybx-omnl/DeployMirrorCoordinator.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {OMNLMirrorCoordinator} from "../../contracts/hybx-omnl/OMNLMirrorCoordinator.sol"; + +/// @notice Deploy OMNLMirrorCoordinator and configure mirror destination (CCIP router + reserve store from env). +contract DeployMirrorCoordinator is Script { + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address admin = vm.addr(pk); + address router = vm.envAddress("OMNL_CCIP_ROUTER"); + address store = vm.envAddress("OMNL_RESERVE_COMMITMENT_STORE"); + address receiver = vm.envAddress("OMNL_MIRROR_RECEIVER"); + uint64 destChainSelector = uint64(vm.envUint("OMNL_CCIP_DEST_CHAIN_SELECTOR")); + address feeToken = vm.envAddress("OMNL_CCIP_FEE_TOKEN"); + + vm.startBroadcast(pk); + OMNLMirrorCoordinator coord = new OMNLMirrorCoordinator(router, store, admin); + coord.setMirrorDestination(destChainSelector, receiver, feeToken); + vm.stopBroadcast(); + + console2.log("OMNLMirrorCoordinator", address(coord)); + } +} diff --git a/script/hybx-omnl/DeployOMNLStack.s.sol b/script/hybx-omnl/DeployOMNLStack.s.sol new file mode 100644 index 0000000..b24d9ae --- /dev/null +++ b/script/hybx-omnl/DeployOMNLStack.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script, console2} from "forge-std/Script.sol"; +import {InstrumentRegistry} from "../../contracts/hybx-omnl/InstrumentRegistry.sol"; +import {ReserveCommitmentStore} from "../../contracts/hybx-omnl/ReserveCommitmentStore.sol"; +import {OMNLCircuitBreaker} from "../../contracts/hybx-omnl/OMNLCircuitBreaker.sol"; +import {ComplianceCore} from "../../contracts/hybx-omnl/ComplianceCore.sol"; + +/// @notice Deploy core OMNL stack (registry, reserves, breakers, compliance). Mirror receiver is chain-specific. +contract DeployOMNLStack is Script { + function run() external { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address admin = vm.addr(pk); + + vm.startBroadcast(pk); + + InstrumentRegistry registry = new InstrumentRegistry(admin); + ReserveCommitmentStore reserves = new ReserveCommitmentStore(admin); + OMNLCircuitBreaker breakers = new OMNLCircuitBreaker(admin); + ComplianceCore core = new ComplianceCore(address(registry), address(reserves), address(breakers)); + + vm.stopBroadcast(); + + console2.log("InstrumentRegistry", address(registry)); + console2.log("ReserveCommitmentStore", address(reserves)); + console2.log("OMNLCircuitBreaker", address(breakers)); + console2.log("ComplianceCore", address(core)); + } +} diff --git a/scripts/deployment/build-gas-pmm-execution-bundle.sh b/scripts/deployment/build-gas-pmm-execution-bundle.sh new file mode 100755 index 0000000..875b8f2 --- /dev/null +++ b/scripts/deployment/build-gas-pmm-execution-bundle.sh @@ -0,0 +1,404 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROJECT_ROOT="$(cd "$REPO_ROOT/.." && pwd)" +BUNDLE_JSON="$REPO_ROOT/config/gas-pmm-execution-bundle.json" +OUT_DIR="$PROJECT_ROOT/reports/status" +OUT_JSON="$OUT_DIR/gas-pmm-execution-bundle-latest.json" +OUT_MD="$OUT_DIR/gas-pmm-execution-bundle-latest.md" + +source "$REPO_ROOT/scripts/lib/deployment/dotenv.sh" +load_deployment_env --repo-root "$REPO_ROOT" + +mkdir -p "$OUT_DIR" + +json_escape() { + jq -Rn --arg v "${1:-}" '$v' +} + +resolve_env_from_list() { + local joined="${1:-}" + local value="" + local key + [[ -z "$joined" ]] && return 0 + IFS='|' read -r -a keys <<< "$joined" + for key in "${keys[@]}"; do + if [[ -n "${!key:-}" ]]; then + value="${!key}" + break + fi + done + printf '%s' "$value" +} + +hex_to_addr() { + local value="${1#0x}" + [[ ${#value} -ge 40 ]] || return 1 + printf '0x%s\n' "${value: -40}" +} + +code_status() { + local rpc="$1" + local address="$2" + if [[ -z "$rpc" || -z "$address" ]]; then + printf 'missing' + return + fi + local code="" + code="$(timeout 12s cast code --rpc-url "$rpc" "$address" 2>/dev/null || true)" + if [[ -n "$code" && "$code" != "0x" ]]; then + printf 'has_code' + else + printf 'no_code' + fi +} + +symbol_call() { + local rpc="$1" + local address="$2" + local value + value="$(timeout 8s cast call --rpc-url "$rpc" "$address" "symbol()(string)" 2>/dev/null || true)" + value="${value#\"}" + value="${value%\"}" + printf '%s' "$value" +} + +normalize_key() { + printf '%s' "$1" | tr '[:lower:]- ' '[:upper:]__' +} + +append_finding() { + local finding="$1" + if [[ -n "$FINDINGS_JOINED" ]]; then + FINDINGS_JOINED+="|$finding" + else + FINDINGS_JOINED="$finding" + fi +} + +emit_findings_json() { + local joined="${1:-}" + if [[ -z "$joined" ]]; then + printf '[]' + return + fi + jq -Rn --arg joined "$joined" '$joined | split("|") | map(select(length > 0))' +} + +rollout_order_json="$(jq -c '[.chains[] | {chainId, network}]' "$BUNDLE_JSON")" +generated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + +json_file="$(mktemp)" +md_file="$(mktemp)" + +{ + echo "{" + echo " \"generatedAt\": $(json_escape "$generated_at")," + echo " \"bundle\": $(jq '.bundleName' "$BUNDLE_JSON")," + echo " \"description\": $(jq '.description' "$BUNDLE_JSON")," + echo " \"poolDefaults\": $(jq '.poolDefaults' "$BUNDLE_JSON")," + echo " \"rolloutOrder\": $rollout_order_json," + echo " \"chains\": [" +} >"$json_file" + +{ + echo "# Gas PMM Execution Bundle" + echo + echo "Generated at: \`$generated_at\`" + echo + echo "This bundle covers the remaining \`22\` planned gas-family DODO PMM rows across \`11\` public chains." + echo + echo "Execution posture:" + echo + echo "- Each chain has two target pools: wrapped-native self-quote and wrapped-native versus stable." + echo "- The current expected pool addresses are still scaffold slots, so \`no_code\` is the normal pre-deploy state." + echo "- Stable-quote pool creation needs a fresh \`STABLE_PRICE_1E18\` input at execution time." + echo "- Wrapped-native quote defaults in this bundle use the repo's canonical or documented live contracts instead of the old \`0xaa...\` placeholders." + echo "- The deploy step reuses the legacy integration constructor slots; those seed addresses are only deployment anchors, while the gas pairs themselves are created through generic \`createPool(...)\` calls." +} >"$md_file" + +first_chain=1 +chain_count=0 +ready_count=0 +blocked_count=0 + +while IFS= read -r chain_row; do + chain_id="$(jq -r '.chainId' <<<"$chain_row")" + network="$(jq -r '.network' <<<"$chain_row")" + chain_key="$(jq -r '.chainKey' <<<"$chain_row")" + rpc_envs_joined="$(jq -r '.rpcEnv | join("|")' <<<"$chain_row")" + integration_env="$(jq -r '.integrationEnv' <<<"$chain_row")" + dvm_envs_joined="$(jq -r '.dodoVendingMachineEnv | join("|")' <<<"$chain_row")" + + rpc="$(resolve_env_from_list "$rpc_envs_joined")" + integration="${!integration_env:-}" + dvm="$(resolve_env_from_list "$dvm_envs_joined")" + + base_symbol="$(jq -r '.baseToken.symbol' <<<"$chain_row")" + base_address="$(jq -r '.baseToken.address' <<<"$chain_row")" + + wrapped_symbol="$(jq -r '.wrappedNativeQuote.symbol' <<<"$chain_row")" + wrapped_envs_joined="$(jq -r '.wrappedNativeQuote.env | join("|")' <<<"$chain_row")" + wrapped_default="$(jq -r '.wrappedNativeQuote.default' <<<"$chain_row")" + wrapped_note="$(jq -r '.wrappedNativeQuote.note' <<<"$chain_row")" + wrapped_address="$(resolve_env_from_list "$wrapped_envs_joined")" + wrapped_address="${wrapped_address:-$wrapped_default}" + + stable_symbol="$(jq -r '.stableQuote.symbol' <<<"$chain_row")" + stable_envs_joined="$(jq -r '.stableQuote.env | join("|")' <<<"$chain_row")" + stable_default="$(jq -r '.stableQuote.default' <<<"$chain_row")" + stable_note="$(jq -r '.stableQuote.note' <<<"$chain_row")" + stable_address="$(resolve_env_from_list "$stable_envs_joined")" + stable_address="${stable_address:-$stable_default}" + + integration_status="$(code_status "$rpc" "$integration")" + dvm_status="$(code_status "$rpc" "$dvm")" + base_status="$(code_status "$rpc" "$base_address")" + wrapped_status="$(code_status "$rpc" "$wrapped_address")" + stable_status="$(code_status "$rpc" "$stable_address")" + + wrapped_pair_row="$(jq -c '.pairs[] | select(.quoteSymbol == $quote)' --arg quote "$wrapped_symbol" <<<"$chain_row")" + stable_pair_row="$(jq -c '.pairs[] | select(.quoteSymbol == $quote)' --arg quote "$stable_symbol" <<<"$chain_row")" + wrapped_pair_name="$(jq -r '.pair' <<<"$wrapped_pair_row")" + stable_pair_name="$(jq -r '.pair' <<<"$stable_pair_row")" + wrapped_pool_expected="$(jq -r '.expectedPoolAddress' <<<"$wrapped_pair_row")" + stable_pool_expected="$(jq -r '.expectedPoolAddress' <<<"$stable_pair_row")" + wrapped_pool_status="$(code_status "$rpc" "$wrapped_pool_expected")" + stable_pool_status="$(code_status "$rpc" "$stable_pool_expected")" + + FINDINGS_JOINED="" + [[ -z "$rpc" ]] && append_finding "missing_rpc" + [[ "$dvm_status" != "has_code" ]] && append_finding "missing_or_invalid_dodo_vending_machine" + [[ "$base_status" != "has_code" ]] && append_finding "gas_mirror_base_missing_or_no_code" + [[ "$wrapped_status" != "has_code" ]] && append_finding "wrapped_native_quote_missing_or_no_code" + [[ "$stable_status" != "has_code" ]] && append_finding "stable_quote_missing_or_no_code" + if [[ -n "$integration" && "$integration_status" == "no_code" ]]; then + append_finding "integration_env_set_but_no_code" + fi + if [[ -z "$integration" || "$integration_status" != "has_code" ]]; then + append_finding "integration_needs_deploy" + fi + [[ "$wrapped_pool_status" != "has_code" ]] && append_finding "wrapped_native_pool_not_live" + [[ "$stable_pool_status" != "has_code" ]] && append_finding "stable_quote_pool_not_live" + + readiness="blocked" + if [[ -n "$rpc" && "$dvm_status" == "has_code" && "$base_status" == "has_code" && "$wrapped_status" == "has_code" && "$stable_status" == "has_code" ]]; then + readiness="ready_for_execution" + fi + if [[ "$readiness" == "ready_for_execution" ]]; then + ready_count=$((ready_count + 1)) + else + blocked_count=$((blocked_count + 1)) + fi + chain_count=$((chain_count + 1)) + + normalized_chain_key="$(normalize_key "$chain_key")" + forge_profile="default" + if [[ "$chain_id" == "25" ]]; then + forge_profile="cronos_legacy" + fi + dvm_primary_env="$(jq -r '.dodoVendingMachineEnv[0]' <<<"$chain_row")" + + deploy_cmd=$(cat <>"$json_file" + fi + first_chain=0 + + cat >>"$json_file" <>"$md_file" +done < <(jq -c '.chains[]' "$BUNDLE_JSON") + +{ + echo + echo " ]," + echo " \"summary\": {" + echo " \"chains\": $chain_count," + echo " \"pairs\": $((chain_count * 2))," + echo " \"readyForExecution\": $ready_count," + echo " \"blocked\": $blocked_count" + echo " }" + echo "}" +} >>"$json_file" + +jq . "$json_file" >"$OUT_JSON" +mv "$md_file" "$OUT_MD" +rm -f "$json_file" + +echo "Wrote:" +echo " $OUT_JSON" +echo " $OUT_MD" diff --git a/scripts/deployment/build-optimism-cronos-dodo-4-pools-bundle.sh b/scripts/deployment/build-optimism-cronos-dodo-4-pools-bundle.sh new file mode 100755 index 0000000..ae31e2a --- /dev/null +++ b/scripts/deployment/build-optimism-cronos-dodo-4-pools-bundle.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROJECT_ROOT="$(cd "$REPO_ROOT/.." && pwd)" +BUNDLE_JSON="$REPO_ROOT/config/optimism-cronos-dodo-4-pools-execution-bundle.json" +OUT_DIR="$PROJECT_ROOT/reports/status" +OUT_JSON="$OUT_DIR/optimism-cronos-dodo-4-pools-execution-bundle-latest.json" +OUT_MD="$OUT_DIR/optimism-cronos-dodo-4-pools-execution-bundle-latest.md" + +source "$REPO_ROOT/scripts/lib/deployment/dotenv.sh" +load_deployment_env --repo-root "$REPO_ROOT" + +mkdir -p "$OUT_DIR" + +json_escape() { + jq -Rn --arg v "${1:-}" '$v' +} + +resolve_env_from_list() { + local joined="$1" + local value="" + local key + IFS='|' read -r -a _keys <<< "$joined" + for key in "${_keys[@]}"; do + if [[ -n "${!key:-}" ]]; then + value="${!key}" + break + fi + done + printf '%s' "$value" +} + +code_status() { + local rpc="$1" + local address="$2" + if [[ -z "$rpc" || -z "$address" ]]; then + printf 'missing' + return + fi + local code="" + code="$(cast code --rpc-url "$rpc" "$address" 2>/dev/null || true)" + if [[ -n "$code" && "$code" != "0x" ]]; then + printf 'has_code' + else + printf 'no_code' + fi +} + +symbol_call() { + local rpc="$1" + local address="$2" + local value + value="$(cast call --rpc-url "$rpc" "$address" "symbol()(string)" 2>/dev/null || true)" + value="${value#\"}" + value="${value%\"}" + printf '%s' "$value" +} + +json_file="$(mktemp)" +md_file="$(mktemp)" + +generated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + +{ + echo "{" + echo " \"generatedAt\": $(json_escape "$generated_at")," + echo " \"bundle\": $(jq '.bundleName' "$BUNDLE_JSON")," + echo " \"chains\": [" +} >"$json_file" + +{ + echo "# Optimism/Cronos DODO 4-Pool Execution Bundle" + echo + echo "Generated at: \`$generated_at\`" + echo + echo "This bundle covers the four remaining \`configured_no_code\` DODO PMM pools:" + echo + echo "- Optimism: \`cWUSDC/USDC\`, \`cWUSDT/USDT\`" + echo "- Cronos: \`cWUSDC/USDC\`, \`cWUSDT/USDT\`" + echo + echo "Execution posture:" + echo + echo "- DODO vending-machine addresses are present in env." + echo "- Compliant \`cWUSDT\` / \`cWUSDC\` token contracts have bytecode on both chains." + echo "- The expected pool addresses still have no code on both chains." + echo "- The canonical quote anchors for Optimism/Cronos are the chain-native official USDT/USDC addresses below." +} >"$md_file" + +first_chain=1 +while IFS= read -r chain_row; do + chain_id="$(jq -r '.chainId' <<<"$chain_row")" + network="$(jq -r '.network' <<<"$chain_row")" + integration_env="$(jq -r '.integrationEnv' <<<"$chain_row")" + dvm_env="$(jq -r '.dodoVendingMachineEnv' <<<"$chain_row")" + rpc_envs_joined="$(jq -r '.rpcEnv | join("|")' <<<"$chain_row")" + + rpc="$(resolve_env_from_list "$rpc_envs_joined")" + dvm="${!dvm_env:-}" + integration="${!integration_env:-}" + + official_usdc_env="$(jq -r '.official.USDC.env' <<<"$chain_row")" + official_usdc_default="$(jq -r '.official.USDC.default' <<<"$chain_row")" + official_usdt_env="$(jq -r '.official.USDT.env' <<<"$chain_row")" + official_usdt_default="$(jq -r '.official.USDT.default' <<<"$chain_row")" + official_usdc="${!official_usdc_env:-$official_usdc_default}" + official_usdt="${!official_usdt_env:-$official_usdt_default}" + + cwusdc_env="$(jq -r '.compliant.cWUSDC.env' <<<"$chain_row")" + cwusdt_env="$(jq -r '.compliant.cWUSDT.env' <<<"$chain_row")" + cwusdc="${!cwusdc_env:-}" + cwusdt="${!cwusdt_env:-}" + + integration_status="$(code_status "$rpc" "$integration")" + dvm_status="$(code_status "$rpc" "$dvm")" + official_usdc_status="$(code_status "$rpc" "$official_usdc")" + official_usdt_status="$(code_status "$rpc" "$official_usdt")" + cwusdc_status="$(code_status "$rpc" "$cwusdc")" + cwusdt_status="$(code_status "$rpc" "$cwusdt")" + + pool_usdc_expected="$(jq -r '.pairs[] | select(.pair=="cWUSDC/USDC") | .expectedPoolAddress' <<<"$chain_row")" + pool_usdt_expected="$(jq -r '.pairs[] | select(.pair=="cWUSDT/USDT") | .expectedPoolAddress' <<<"$chain_row")" + pool_usdc_status="$(code_status "$rpc" "$pool_usdc_expected")" + pool_usdt_status="$(code_status "$rpc" "$pool_usdt_expected")" + forge_profile="default" + if [[ "$chain_id" == "25" ]]; then + forge_profile="cronos_legacy" + fi + + deploy_cmd=$(cat <>"$json_file" + fi + first_chain=0 + + cat >>"$json_file" <>"$md_file" +done < <(jq -c '.chains[]' "$BUNDLE_JSON") + +{ + echo + echo " ]" + echo "}" +} >>"$json_file" + +jq . "$json_file" >"$OUT_JSON" +mv "$md_file" "$OUT_MD" +rm -f "$json_file" + +echo "Wrote:" +echo " $OUT_JSON" +echo " $OUT_MD" diff --git a/scripts/deployment/c138-cw-bridge-mainnet-pct.sh b/scripts/deployment/c138-cw-bridge-mainnet-pct.sh new file mode 100755 index 0000000..8a67646 --- /dev/null +++ b/scripts/deployment/c138-cw-bridge-mainnet-pct.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +# Chain 138 → Ethereum Mainnet only: bridge PCT of each canonical c* balance via CWMultiTokenBridgeL1. +# Default PCT: 17.25% (= 1725 basis points of 10000; override with PCT_BP). +# +# Modes: +# --plan-only Write JSON + summary (default) +# --check-routes destinations(token, Mainnet selector) for each token +# --emit-cmds Print approve + lockAndSend cast lines (review before running) +# +# Env: PRIVATE_KEY (for --emit-cmds deployer address), RPC_URL_138, CW_L1_BRIDGE_CHAIN138, +# LINK_TOKEN_CHAIN138, RECIPIENT_ADDRESS (default: deployer) +# PCT_BP (default 1725 = 17.25%) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +MODE="plan" +while [[ $# -gt 0 ]]; do + case "$1" in + --plan-only) MODE="plan" ;; + --check-routes) MODE="check" ;; + --emit-cmds) MODE="emit" ;; + --help|-h) + grep '^#' "$0" | head -22 + exit 0 + ;; + *) echo "Unknown: $1"; exit 1 ;; + esac + shift || true +done + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +PCT_BP="${PCT_BP:-1725}" +RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}" +OUT_JSON="${OUT_JSON:-$SMOM_ROOT/reports/status/c138-bridge-mainnet-pct-latest.json}" +BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}" +DEPLOYER="" +if [[ -n "${PRIVATE_KEY:-}" ]]; then + DEPLOYER="$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true)" +fi +RECIPIENT="${RECIPIENT_ADDRESS:-$DEPLOYER}" +MAINNET_SEL=5009297550715157269 + +export RPC OUT_JSON DEPLOYER RECIPIENT BRIDGE PCT_BP MAINNET_SEL + +# Canonical c* only (EXPLORER_TOKEN_LIST_CROSSCHECK §5) +read -r -d '' TOKEN_ROWS << 'EOF' || true +cUSDT:0x93E66202A11B1772E55407B32B44e5Cd8eda7f22 +cUSDC:0xf22258f57794CC8E06237084b353Ab30fFfa640b +cEURC:0x8085961F9cF02b4d800A3c6d386D31da4B34266a +cEURT:0xdf4b71c61E5912712C1Bdd451416B9aC26949d72 +cGBPC:0x003960f16D9d34F2e98d62723B6721Fb92074aD2 +cGBPT:0x350f54e4D23795f86A9c03988c7135357CCaD97c +cAUDC:0xD51482e567c03899eecE3CAe8a058161FD56069D +cJPYC:0xEe269e1226a334182aace90056EE4ee5Cc8A6770 +cCHFC:0x873990849DDa5117d7C644f0aF24370797C03885 +cCADC:0x54dBd40cF05e15906A2C21f600937e96787f5679 +cXAUC:0x290E52a8819A4fbD0714E517225429aA2B70EC6b +cXAUT:0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E +EOF +export TOKEN_ROWS +mkdir -p "$(dirname "$OUT_JSON")" + +python3 << PY +import json, subprocess, os, re + +rpc = os.environ.get("RPC", "http://192.168.11.211:8545") +deployer = os.environ.get("DEPLOYER", "") +recipient = os.environ.get("RECIPIENT", deployer) +bridge = os.environ.get("BRIDGE", "") +pct_bp = int(os.environ.get("PCT_BP", "1725")) +sel = int(os.environ.get("MAINNET_SEL", "5009297550715157269")) + +rows = [] +for line in os.environ.get("TOKEN_ROWS", "").strip().split("\n"): + if not line.strip(): + continue + sym, addr = line.split(":", 1) + rows.append((sym.strip(), addr.strip())) + +def balance_of(addr): + if not deployer: + return None + r = subprocess.run( + ["cast", "call", addr, "balanceOf(address)(uint256)", deployer, "--rpc-url", rpc], + capture_output=True, text=True, + ) + if r.returncode != 0: + return None + m = re.match(r"^\s*(\d+)", r.stdout.strip()) + return int(m.group(1)) if m else None + +plan = { + "schema": "c138-bridge-mainnet-pct/v1", + "rpc_url": rpc, + "deployer": deployer, + "recipient": recipient, + "cw_l1_bridge": bridge, + "destination": "Ethereum Mainnet", + "chain_selector": str(sel), + "pct_basis_points": pct_bp, + "pct_human": f"{pct_bp / 100:.2f}%", + "tokens": [], +} + +for sym, addr in rows: + bal = balance_of(addr) + if bal is None: + plan["tokens"].append({"symbol": sym, "address": addr, "error": "balance_of_failed"}) + continue + amt = bal * pct_bp // 10000 + plan["tokens"].append({ + "symbol": sym, + "address": addr, + "balance_wei": str(bal), + "amount_to_bridge_wei": str(amt), + }) + +path = os.environ.get("OUT_JSON", "") +with open(path, "w") as f: + json.dump(plan, f, indent=2) + +print(json.dumps({"written": path, "tokens": len(plan["tokens"])})) +PY + +python3 - <<'PY' +import json, os +with open(os.environ["OUT_JSON"]) as f: + p = json.load(f) +print("\n=== c* → Mainnet only:", p.get("pct_human"), "of balance (integer base units) ===\n") +print(f"Deployer: {p.get('deployer','?')}\nRecipient: {p.get('recipient','?')}\nBridge: {p.get('cw_l1_bridge') or '(unset)'}\n") +for t in p["tokens"]: + if "error" in t: + print(f"{t['symbol']}: {t['error']}") + continue + sym = t["symbol"] + amt = int(t["amount_to_bridge_wei"]) + if sym.startswith("cXAU"): + print(f"{sym:<10} bridge: {amt / 1e6:,.6f} troy oz (wei={t['amount_to_bridge_wei']})") + else: + print(f"{sym:<10} bridge: {amt / 1e6:,.6f} tokens (wei={t['amount_to_bridge_wei']})") +print("\nJSON:", os.environ["OUT_JSON"]) +PY + +if [[ "$MODE" == "plan" ]]; then + exit 0 +fi + +[[ -n "$BRIDGE" ]] || { echo "Set CW_L1_BRIDGE_CHAIN138"; exit 1; } +code=$(cast code "$BRIDGE" --rpc-url "$RPC" 2>/dev/null || echo "0x") +[[ -n "$code" && "$code" != "0x" ]] || { echo "No contract at CW_L1_BRIDGE_CHAIN138=$BRIDGE"; exit 1; } + +if [[ "$MODE" == "check" ]]; then + echo "" + echo "=== Mainnet route: $BRIDGE ===" + while IFS= read -r line; do + [[ -z "$line" ]] && continue + sym="${line%%:*}" + addr="${line#*:}" + dest=$(cast call "$BRIDGE" "destinations(address,uint64)(address,bool)" "$addr" "$MAINNET_SEL" --rpc-url "$RPC" 2>/dev/null || echo "ERR") + echo "$sym ($addr): destinations(Mainnet)=$dest" + done <<< "$TOKEN_ROWS" + exit 0 +fi + +if [[ "$MODE" == "emit" ]]; then + [[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required for --emit-cmds"; exit 1; } + [[ -n "$RECIPIENT" ]] || { echo "RECIPIENT_ADDRESS or deployer required"; exit 1; } + LINK_TOKEN="${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}" + [[ -n "$LINK_TOKEN" ]] || { echo "Set LINK_TOKEN or LINK_TOKEN_CHAIN138 for fee approval lines"; exit 1; } + export LINK_TOKEN + OUT_CAST="${OUT_CAST:-$SMOM_ROOT/reports/status/c138-bridge-mainnet-pct-cast-commands.sh}" + export OUT_CAST + { + echo "#!/usr/bin/env bash" + echo "# Generated: c138-cw-bridge-mainnet-pct.sh --emit-cmds" + echo "# Review fee + reserve verifier. Fund LINK on deployer for CCIP fees." + echo "set -euo pipefail" + python3 <<'PY' +import json, os, subprocess +rpc = os.environ["RPC"] +bridge = os.environ["BRIDGE"] +recipient = os.environ["RECIPIENT"] +link = os.environ["LINK_TOKEN"] +sel = int(os.environ["MAINNET_SEL"]) +with open(os.environ["OUT_JSON"]) as f: + plan = json.load(f) +for t in plan["tokens"]: + if "error" in t: + continue + amt = t.get("amount_to_bridge_wei", "0") + if int(amt) == 0: + continue + sym, token = t["symbol"], t["address"] + chk = subprocess.run( + ["cast", "call", bridge, "destinations(address,uint64)(address,bool)", token, str(sel), "--rpc-url", rpc], + capture_output=True, text=True, + ) + if chk.returncode != 0 or "true" not in chk.stdout: + print(f"# SKIP {sym}: destination Mainnet not enabled or query failed") + print(f"# cast output: {chk.stdout.strip()}") + continue + fee = subprocess.run( + ["cast", "call", bridge, + "calculateFee(address,uint64,address,uint256)(uint256)", + token, str(sel), recipient, amt, + "--rpc-url", rpc], + capture_output=True, text=True, + ) + fq = fee.stdout.strip().split()[0] if fee.returncode == 0 else "0" + print("") + print(f"# {sym} -> Mainnet amount={amt} fee_wei={fq}") + print(f"cast send {link} \"approve(address,uint256)\" {bridge} {fq} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 120000") + print(f"cast send {token} \"approve(address,uint256)\" {bridge} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 400000") + print(f"cast send {bridge} \"lockAndSend(address,uint64,address,uint256)\" {token} {sel} {recipient} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 4000000") +PY + } > "$OUT_CAST" + chmod +x "$OUT_CAST" + echo "Wrote: $OUT_CAST" + wc -l "$OUT_CAST" + exit 0 +fi diff --git a/scripts/deployment/configure-cw-public-bridge-mesh.sh b/scripts/deployment/configure-cw-public-bridge-mesh.sh new file mode 100755 index 0000000..a546a13 --- /dev/null +++ b/scripts/deployment/configure-cw-public-bridge-mesh.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +if [[ -f "$REPO_ROOT/scripts/load-env.sh" ]]; then + # shellcheck disable=SC1090 + source "$REPO_ROOT/scripts/load-env.sh" >/dev/null +elif [[ -f "$REPO_ROOT/../scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + source "$REPO_ROOT/../scripts/lib/load-project-env.sh" >/dev/null +fi + +APPLY=false +CHAIN_FILTER="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) APPLY=true ;; + --chain=*) CHAIN_FILTER="${1#*=}" ;; + --chain) + CHAIN_FILTER="${2:-}" + shift + ;; + *) + echo "Unknown arg: $1" >&2 + echo "Usage: $0 [--apply] [--chain ]" >&2 + exit 1 + ;; + esac + shift +done + +declare -A CHAIN_NAME=( + [1]="Ethereum Mainnet" + [10]="Optimism" + [25]="Cronos" + [56]="BSC" + [100]="Gnosis" + [137]="Polygon" + [8453]="Base" + [42161]="Arbitrum" + [42220]="Celo" + [43114]="Avalanche" +) + +declare -A CHAIN_SELECTOR=( + [1]="5009297550715157269" + [10]="3734403246176062136" + [25]="1456215246176062136" + [56]="11344663589394136015" + [100]="465200170687744372" + [137]="4051577828743386545" + [8453]="15971525489660198786" + [42161]="4949039107694359620" + [42220]="1346049177634351622" + [43114]="6433500567565415381" +) + +rpc_for_chain() { + case "$1" in + 1) echo "${MAINNET_RPC_URL:-${ETHEREUM_MAINNET_RPC:-${ETHEREUM_RPC_URL:-}}}" ;; + 10) echo "${OPTIMISM_RPC_URL:-${OPTIMISM_MAINNET_RPC:-}}" ;; + 25) echo "${CRONOS_RPC_URL:-}" ;; + 56) echo "${BSC_RPC_URL:-${BSC_MAINNET_RPC:-}}" ;; + 100) echo "${GNOSIS_RPC_URL:-${GNOSIS_MAINNET_RPC:-${GNOSIS_RPC:-}}}" ;; + 137) echo "${POLYGON_RPC_URL:-${POLYGON_MAINNET_RPC:-}}" ;; + 8453) echo "${BASE_RPC_URL:-${BASE_MAINNET_RPC:-}}" ;; + 42161) echo "${ARBITRUM_RPC_URL:-${ARBITRUM_MAINNET_RPC:-}}" ;; + 42220) echo "${CELO_RPC_URL:-}" ;; + 43114) echo "${AVALANCHE_RPC_URL:-${AVALANCHE_MAINNET_RPC:-}}" ;; + *) echo "" ;; + esac +} + +bridge_for_chain() { + case "$1" in + 1) echo "${CW_BRIDGE_MAINNET:-}" ;; + 10) echo "${CW_BRIDGE_OPTIMISM:-}" ;; + 25) echo "${CW_BRIDGE_CRONOS:-}" ;; + 56) echo "${CW_BRIDGE_BSC:-}" ;; + 100) echo "${CW_BRIDGE_GNOSIS:-}" ;; + 137) echo "${CW_BRIDGE_POLYGON:-}" ;; + 8453) echo "${CW_BRIDGE_BASE:-}" ;; + 42161) echo "${CW_BRIDGE_ARBITRUM:-}" ;; + 42220) echo "${CW_BRIDGE_CELO:-}" ;; + 43114) echo "${CW_BRIDGE_AVALANCHE:-}" ;; + *) echo "" ;; + esac +} + +probe_generation() { + local bridge="$1" rpc="$2" + if timeout 10 cast call "$bridge" 'sendRouter()(address)' --rpc-url "$rpc" >/dev/null 2>&1; then + echo "new" + return + fi + if timeout 10 cast call "$bridge" 'ccipRouter()(address)' --rpc-url "$rpc" >/dev/null 2>&1; then + echo "old" + return + fi + echo "unknown" +} + +old_has_key() { + local bridge="$1" rpc="$2" key="$3" + local destinations + destinations="$(timeout 12 cast call "$bridge" 'getDestinationChains()(uint64[])' --rpc-url "$rpc" 2>/dev/null || true)" + [[ -z "$destinations" ]] && return 1 + + DESTINATIONS_PAYLOAD="$destinations" python3 - "$key" <<'PY' +import re +import os +import sys + +target = sys.argv[1] +payload = os.environ.get("DESTINATIONS_PAYLOAD", "") +numbers = re.findall(r'\d+', payload) +sys.exit(0 if target in numbers else 1) +PY +} + +new_read_destination() { + local bridge="$1" rpc="$2" key="$3" + timeout 12 cast call "$bridge" 'destinations(uint64)((address,bool))' "$key" --rpc-url "$rpc" 2>/dev/null || true +} + +normalize_addr() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' +} + +new_matches() { + local raw="$1" expected="$2" + local lower_raw lower_expected + lower_raw="$(normalize_addr "$raw")" + lower_expected="$(normalize_addr "$expected")" + [[ "$lower_raw" == *"$lower_expected"* && "$lower_raw" == *"true"* ]] +} + +send_tx() { + local rpc="$1" bridge="$2" signature="$3" + shift 3 + cast send "$bridge" "$signature" "$@" --rpc-url "$rpc" --private-key "$PRIVATE_KEY" +} + +configure_old() { + local source_chain="$1" target_key="$2" target_bridge="$3" + local rpc bridge op + rpc="$(rpc_for_chain "$source_chain")" + bridge="$(bridge_for_chain "$source_chain")" + if old_has_key "$bridge" "$rpc" "$target_key"; then + op="updateDestination(uint64,address)" + else + op="addDestination(uint64,address)" + fi + if [[ "$APPLY" == true ]]; then + send_tx "$rpc" "$bridge" "$op" "$target_key" "$target_bridge" + else + echo "DRY-RUN old chain=$source_chain key=$target_key bridge=$target_bridge op=$op" + fi +} + +configure_new() { + local source_chain="$1" target_key="$2" target_bridge="$3" + local rpc bridge raw + rpc="$(rpc_for_chain "$source_chain")" + bridge="$(bridge_for_chain "$source_chain")" + raw="$(new_read_destination "$bridge" "$rpc" "$target_key")" + if new_matches "$raw" "$target_bridge"; then + echo "SKIP new chain=$source_chain key=$target_key already=$raw" + return + fi + if [[ "$APPLY" == true ]]; then + send_tx "$rpc" "$bridge" 'configureDestination(uint64,address,bool)' "$target_key" "$target_bridge" true + else + echo "DRY-RUN new chain=$source_chain key=$target_key bridge=$target_bridge" + fi +} + +configure_key() { + local source_chain="$1" target_key="$2" target_bridge="$3" generation="$4" + case "$generation" in + old) configure_old "$source_chain" "$target_key" "$target_bridge" ;; + new) configure_new "$source_chain" "$target_key" "$target_bridge" ;; + *) + echo "ERROR unknown generation for chain $source_chain" >&2 + return 1 + ;; + esac +} + +echo "Public cW bridge mesh configuration" +echo "Apply mode: $APPLY" +echo + +declare -A GENERATION=() + +for chain_id in 1 10 25 56 100 137 8453 42161 42220 43114; do + [[ -n "$CHAIN_FILTER" && "$CHAIN_FILTER" != "$chain_id" ]] && continue + rpc="$(rpc_for_chain "$chain_id")" + bridge="$(bridge_for_chain "$chain_id")" + if [[ -z "$rpc" || -z "$bridge" ]]; then + echo "WARN chain=$chain_id missing rpc or bridge env; skipping" + continue + fi + generation="$(probe_generation "$bridge" "$rpc")" + GENERATION["$chain_id"]="$generation" + echo "chain=$chain_id name='${CHAIN_NAME[$chain_id]}' generation=$generation bridge=$bridge" +done + +echo + +for source_chain in 1 10 25 56 100 137 8453 42161 42220 43114; do + [[ -n "$CHAIN_FILTER" && "$CHAIN_FILTER" != "$source_chain" ]] && continue + [[ -z "${GENERATION[$source_chain]:-}" ]] && continue + for target_chain in 1 10 25 56 100 137 8453 42161 42220 43114; do + [[ "$source_chain" == "$target_chain" ]] && continue + target_bridge="$(bridge_for_chain "$target_chain")" + [[ -z "$target_bridge" ]] && continue + + # Outbound path on source uses the target CCIP selector. + configure_key "$source_chain" "${CHAIN_SELECTOR[$target_chain]}" "$target_bridge" "${GENERATION[$source_chain]}" + + # Inbound receive path on source uses the remote public chain id. + configure_key "$source_chain" "$target_chain" "$target_bridge" "${GENERATION[$source_chain]}" + done +done + +echo +echo "Done." diff --git a/scripts/deployment/create-uniswap-v3-gas-pool.sh b/scripts/deployment/create-uniswap-v3-gas-pool.sh new file mode 100755 index 0000000..909ef10 --- /dev/null +++ b/scripts/deployment/create-uniswap-v3-gas-pool.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "$REPO_ROOT" +fi + +FACTORY="${FACTORY:-${UNISWAP_V3_FACTORY:-}}" +RPC_URL="${RPC_URL:-}" +TOKEN_A="${TOKEN_A:-}" +TOKEN_B="${TOKEN_B:-}" +FEE="${FEE:-500}" +EXECUTE="${EXECUTE:-0}" +SQRT_PRICE_X96="${SQRT_PRICE_X96:-79228162514264337593543950336}" +PRIVATE_KEY="${PRIVATE_KEY:-}" + +if [[ -z "$FACTORY" || -z "$RPC_URL" || -z "$TOKEN_A" || -z "$TOKEN_B" ]]; then + echo "Required: FACTORY RPC_URL TOKEN_A TOKEN_B" >&2 + exit 1 +fi + +get_pool() { + cast call "$FACTORY" \ + "getPool(address,address,uint24)(address)" \ + "$TOKEN_A" "$TOKEN_B" "$FEE" \ + --rpc-url "$RPC_URL" 2>/dev/null || true +} + +pool="$(get_pool)" +pool="${pool//$'\n'/}" + +if [[ -z "$pool" || "$pool" == "0x0000000000000000000000000000000000000000" ]]; then + echo "Pool does not exist yet for $TOKEN_A / $TOKEN_B fee $FEE" + if [[ "$EXECUTE" != "1" ]]; then + echo "Dry run: set EXECUTE=1 to call createPool on $FACTORY" + exit 0 + fi + if [[ -z "$PRIVATE_KEY" ]]; then + echo "PRIVATE_KEY is required when EXECUTE=1" >&2 + exit 1 + fi + cast send "$FACTORY" \ + "createPool(address,address,uint24)" \ + "$TOKEN_A" "$TOKEN_B" "$FEE" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + -vv + pool="$(get_pool)" + pool="${pool//$'\n'/}" +fi + +echo "Pool address: $pool" + +slot0="$(cast call "$pool" "slot0()((uint160,int24,uint16,uint16,uint16,uint8,bool))" --rpc-url "$RPC_URL" 2>/dev/null || true)" +slot0_no_ws="$(printf '%s' "$slot0" | tr -d '[:space:]')" + +if [[ "$slot0_no_ws" == "(0,0,0,0,0,0,false)" || -z "$slot0_no_ws" ]]; then + echo "Pool is not initialized" + if [[ "$EXECUTE" != "1" ]]; then + echo "Dry run: set EXECUTE=1 to initialize at sqrtPriceX96=$SQRT_PRICE_X96" + exit 0 + fi + if [[ -z "$PRIVATE_KEY" ]]; then + echo "PRIVATE_KEY is required when EXECUTE=1" >&2 + exit 1 + fi + cast send "$pool" \ + "initialize(uint160)" \ + "$SQRT_PRICE_X96" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + -vv + echo "Initialized pool at sqrtPriceX96=$SQRT_PRICE_X96" +else + echo "Pool already initialized: $slot0" +fi diff --git a/scripts/deployment/cw-enforce-bridge-only-roles.sh b/scripts/deployment/cw-enforce-bridge-only-roles.sh new file mode 100755 index 0000000..74b2a14 --- /dev/null +++ b/scripts/deployment/cw-enforce-bridge-only-roles.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +# Reconcile cW* token roles so only the configured per-network bridge keeps MINTER/BURNER. +# +# The script is intentionally conservative: +# - It grants MINTER/BURNER to the configured bridge if missing +# - It revokes MINTER/BURNER from the deployer/admin by default +# - It can revoke MINTER/BURNER from an explicit denylist of extra addresses +# - It can optionally freeze future operational role changes on newer cW* contracts +# +# Usage: +# bash scripts/deployment/cw-enforce-bridge-only-roles.sh --dry-run +# bash scripts/deployment/cw-enforce-bridge-only-roles.sh +# +# Optional env: +# CW_ROLE_CHAINS="1 10 25 56 100 137 42161 42220 43114 8453" +# CW_ROLE_TOKENS="CWUSDT CWUSDC CWEURC" +# CW_ROLE_EXTRA_REVOKE="0xabc...,0xdef..." +# CW_ROLE_KEEP_ALLOWLIST="0xabc...,0xdef..." +# CW_ROLE_REVOKE_DEPLOYER=1 # default 1 +# CW_ROLE_FREEZE_IF_SUPPORTED=1 # default 1 +# CW_ROLE_FROM_BLOCK=earliest # optional log scan start block for role event discovery +# PRIVATE_KEY # required unless --dry-run + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)" +cd "$SMOM_ROOT" + +DRY_RUN=0 +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=1 +fi + +if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then + # shellcheck disable=SC1090 + PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" +elif [[ -f .env ]]; then + set -a && source .env && set +a +fi + +if ! command -v cast >/dev/null 2>&1; then + echo "cast is required" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required" >&2 + exit 1 +fi + +CHAIN_ROWS=( + "1|MAINNET|ETHEREUM_MAINNET_RPC|CW_BRIDGE_MAINNET" + "10|OPTIMISM|OPTIMISM_MAINNET_RPC|CW_BRIDGE_OPTIMISM" + "25|CRONOS|CRONOS_MAINNET_RPC|CW_BRIDGE_CRONOS" + "56|BSC|BSC_MAINNET_RPC|CW_BRIDGE_BSC" + "100|GNOSIS|GNOSIS_MAINNET_RPC|CW_BRIDGE_GNOSIS" + "137|POLYGON|POLYGON_MAINNET_RPC|CW_BRIDGE_POLYGON" + "42161|ARBITRUM|ARBITRUM_MAINNET_RPC|CW_BRIDGE_ARBITRUM" + "42220|CELO|CELO_MAINNET_RPC|CW_BRIDGE_CELO" + "43114|AVALANCHE|AVALANCHE_MAINNET_RPC|CW_BRIDGE_AVALANCHE" + "8453|BASE|BASE_MAINNET_RPC|CW_BRIDGE_BASE" +) + +TOKENS=( + "CWUSDT" + "CWUSDC" + "CWAUSDT" + "CWUSDW" + "CWEURC" + "CWEURT" + "CWGBPC" + "CWGBPT" + "CWAUDC" + "CWJPYC" + "CWCHFC" + "CWCADC" + "CWXAUC" + "CWXAUT" +) + +CHAIN_FILTER="${CW_ROLE_CHAINS:-1 10 25 56 100 137 42161 42220 43114 8453}" +TOKEN_FILTER="${CW_ROLE_TOKENS:-}" +REVOKE_DEPLOYER="${CW_ROLE_REVOKE_DEPLOYER:-1}" +FREEZE_IF_SUPPORTED="${CW_ROLE_FREEZE_IF_SUPPORTED:-1}" +EXTRA_REVOKE_RAW="${CW_ROLE_EXTRA_REVOKE:-}" +KEEP_ALLOWLIST_RAW="${CW_ROLE_KEEP_ALLOWLIST:-}" +ROLE_FROM_BLOCK="${CW_ROLE_FROM_BLOCK:-earliest}" + +MINTER_ROLE="$(cast keccak "MINTER_ROLE")" +BURNER_ROLE="$(cast keccak "BURNER_ROLE")" +DEFAULT_ADMIN_ROLE="0x0000000000000000000000000000000000000000000000000000000000000000" + +send_cmd() { + if [[ "$DRY_RUN" -eq 1 ]]; then + local rendered=() + local redact_next=0 + local arg + for arg in "$@"; do + if [[ "$redact_next" -eq 1 ]]; then + rendered+=("") + redact_next=0 + continue + fi + if [[ "$arg" == "--private-key" ]]; then + rendered+=("$arg") + redact_next=1 + continue + fi + rendered+=("$arg") + done + echo "[dry-run] ${rendered[*]}" + return 0 + fi + "$@" +} + +bool_call() { + local rpc="$1" + local contract="$2" + local sig="$3" + shift 3 + cast call "$contract" "$sig" "$@" --rpc-url "$rpc" 2>/dev/null +} + +normalize_address() { + local value="${1,,}" + value="${value#0x}" + if [[ ${#value} -lt 40 ]]; then + return 1 + fi + echo "0x${value: -40}" +} + +append_unique_address() { + local __var_name="$1" + local candidate="${2,,}" + [[ -n "$candidate" ]] || return 0 + local current="${!__var_name:-}" + if [[ " $current " != *" $candidate "* ]]; then + printf -v "$__var_name" '%s%s ' "$current" "$candidate" + fi +} + +load_csv_addresses() { + local raw="$1" + local __out_var="$2" + local item normalized + IFS=',' read -r -a _addr_items <<< "$raw" + for item in "${_addr_items[@]}"; do + item="${item//[[:space:]]/}" + [[ -n "$item" ]] || continue + normalized="$(normalize_address "$item" || true)" + [[ -n "$normalized" ]] || continue + append_unique_address "$__out_var" "$normalized" + done +} + +discover_role_holders() { + local rpc="$1" + local token="$2" + local role="$3" + cast logs "RoleGranted(bytes32,address,address)" "$role" \ + --from-block "$ROLE_FROM_BLOCK" --to-block latest --address "$token" --rpc-url "$rpc" --json 2>/dev/null | + jq -r '.[].topics[2] // empty' | + while read -r topic; do + normalize_address "$topic" || true + done +} + +function_exists() { + local rpc="$1" + local contract="$2" + local sig="$3" + cast call "$contract" "$sig" --rpc-url "$rpc" >/dev/null 2>&1 +} + +grant_role_if_supported() { + local rpc="$1" + local token="$2" + local role="$3" + local target="$4" + local current + current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$target" || true)" + if [[ "$current" != "true" ]]; then + send_cmd cast send "$token" "grantRole(bytes32,address)" "$role" "$target" \ + --rpc-url "$rpc" --private-key "$PRIVATE_KEY" + fi +} + +revoke_role_if_present() { + local rpc="$1" + local token="$2" + local role="$3" + local target="$4" + local current + current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$target" || true)" + if [[ "$current" == "true" ]]; then + send_cmd cast send "$token" "revokeRole(bytes32,address)" "$role" "$target" \ + --rpc-url "$rpc" --private-key "$PRIVATE_KEY" + fi +} + +role_label() { + case "$1" in + "$DEFAULT_ADMIN_ROLE") echo "DEFAULT_ADMIN_ROLE" ;; + "$MINTER_ROLE") echo "MINTER_ROLE" ;; + "$BURNER_ROLE") echo "BURNER_ROLE" ;; + *) echo "$1" ;; + esac +} + +for row in "${CHAIN_ROWS[@]}"; do + IFS='|' read -r chain_id chain_key rpc_var bridge_var <<< "$row" + if [[ " $CHAIN_FILTER " != *" $chain_id "* ]]; then + continue + fi + + rpc="${!rpc_var:-}" + bridge="${!bridge_var:-}" + deployer="${DEPLOYER_ADDRESS:-${DEPLOYER_WALLET:-${DEPLOYER_EOA:-${DEFAULT_FROM_ADDRESS:-}}}}" + if [[ -z "$deployer" && -n "${PRIVATE_KEY:-}" ]]; then + deployer="$(cast wallet address --private-key "$PRIVATE_KEY" 2>/dev/null || true)" + fi + bridge="$(normalize_address "$bridge" || true)" + deployer="$(normalize_address "$deployer" || true)" + + if [[ -z "$rpc" || -z "$bridge" ]]; then + echo "Skip chain $chain_id ($chain_key): missing rpc or bridge env" + continue + fi + + echo "=== Chain $chain_id ($chain_key) ===" + echo "Bridge: $bridge" + + for token_prefix in "${TOKENS[@]}"; do + if [[ -n "$TOKEN_FILTER" && " $TOKEN_FILTER " != *" $token_prefix "* ]]; then + continue + fi + token_var="${token_prefix}_${chain_key}" + token="${!token_var:-}" + if [[ -z "$token" || "$token" == "0x0000000000000000000000000000000000000000" ]]; then + continue + fi + + echo "-- $token_var $token" + + grant_role_if_supported "$rpc" "$token" "$MINTER_ROLE" "$bridge" + grant_role_if_supported "$rpc" "$token" "$BURNER_ROLE" "$bridge" + + keep_allowlist="" + append_unique_address keep_allowlist "$bridge" + load_csv_addresses "$KEEP_ALLOWLIST_RAW" keep_allowlist + + revoke_candidates="" + while read -r discovered; do + [[ -n "$discovered" ]] || continue + append_unique_address revoke_candidates "$discovered" + done < <(discover_role_holders "$rpc" "$token" "$MINTER_ROLE") + while read -r discovered; do + [[ -n "$discovered" ]] || continue + append_unique_address revoke_candidates "$discovered" + done < <(discover_role_holders "$rpc" "$token" "$BURNER_ROLE") + + if [[ "$REVOKE_DEPLOYER" == "1" && -n "$deployer" ]]; then + append_unique_address revoke_candidates "$deployer" + fi + load_csv_addresses "$EXTRA_REVOKE_RAW" revoke_candidates + + for addr in $revoke_candidates; do + [[ " $keep_allowlist " == *" ${addr,,} "* ]] && continue + revoke_role_if_present "$rpc" "$token" "$MINTER_ROLE" "$addr" + revoke_role_if_present "$rpc" "$token" "$BURNER_ROLE" "$addr" + done + + if [[ "$FREEZE_IF_SUPPORTED" == "1" ]]; then + if function_exists "$rpc" "$token" "operationalRolesFrozen()(bool)"; then + frozen="$(cast call "$token" "operationalRolesFrozen()(bool)" --rpc-url "$rpc" 2>/dev/null || true)" + if [[ "$frozen" != "true" ]]; then + send_cmd cast send "$token" "freezeOperationalRoles()" \ + --rpc-url "$rpc" --private-key "$PRIVATE_KEY" --gas-limit 80000 + fi + fi + fi + + for role in "$DEFAULT_ADMIN_ROLE" "$MINTER_ROLE" "$BURNER_ROLE"; do + summary_holders="" + while read -r discovered; do + [[ -n "$discovered" ]] || continue + current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$discovered" || true)" + if [[ "$current" == "true" ]]; then + append_unique_address summary_holders "$discovered" + fi + done < <(discover_role_holders "$rpc" "$token" "$role") + echo " $(role_label "$role"): ${summary_holders:-}" + done + done +done diff --git a/scripts/deployment/deploy-chain138-gas-canonicals.sh b/scripts/deployment/deploy-chain138-gas-canonicals.sh new file mode 100644 index 0000000..34282e0 --- /dev/null +++ b/scripts/deployment/deploy-chain138-gas-canonicals.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "$REPO_ROOT" +fi + +RPC_URL="${RPC_URL_138:-${CHAIN138_RPC:-http://192.168.11.211:8545}}" +PRIVATE_KEY="${PRIVATE_KEY:-}" +GAS_PRICE="${GAS_PRICE:-1000000000}" +EXECUTE="${EXECUTE:-0}" +ONLY_FAMILY="${GAS_FAMILY:-${1:-}}" + +if [[ -z "$PRIVATE_KEY" ]]; then + echo "PRIVATE_KEY is required" >&2 + exit 1 +fi + +cmd=( + bash scripts/forge/scope.sh script tokens + script/deploy/DeployGasCanonicalTokens.s.sol:DeployGasCanonicalTokens + --rpc-url "$RPC_URL" + --broadcast + --private-key "$PRIVATE_KEY" + --legacy + --with-gas-price "$GAS_PRICE" + -vvv +) + +export FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-chain138_legacy}" +if [[ -n "$ONLY_FAMILY" ]]; then + export GAS_FAMILY="$ONLY_FAMILY" +fi + +echo "Chain 138 gas-canonical deploy" +echo " profile: $FOUNDRY_PROFILE" +echo " rpc: $RPC_URL" +echo " gas price: $GAS_PRICE" +echo " family: ${GAS_FAMILY:-all}" + +if [[ "$EXECUTE" != "1" ]]; then + echo "Dry run only. Re-run with EXECUTE=1 to broadcast." + printf ' %q' "${cmd[@]}" + echo + exit 0 +fi + +cd "$REPO_ROOT" +"${cmd[@]}" diff --git a/scripts/deployment/fund-chain138-eth-pmm-pools.sh b/scripts/deployment/fund-chain138-eth-pmm-pools.sh new file mode 100644 index 0000000..54a3646 --- /dev/null +++ b/scripts/deployment/fund-chain138-eth-pmm-pools.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$SMOM_ROOT" + +if [[ -f "$SMOM_ROOT/scripts/lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1091 + source "$SMOM_ROOT/scripts/lib/deployment/dotenv.sh" + load_deployment_env --repo-root "$SMOM_ROOT" +else + source "$SMOM_ROOT/scripts/load-env.sh" >/dev/null 2>&1 || true +fi + +PLAN_JSON="${PLAN_JSON:-$SMOM_ROOT/config/chain138-eth-pmm-liquidity-plan.json}" +PROFILE="${PROFILE:-}" +EXECUTE="${EXECUTE:-0}" +RPC_URL_138="${RPC_URL_138:-${RPC_URL:-http://192.168.11.211:8545}}" +DODO_PMM_INTEGRATION_ADDRESS="${DODO_PMM_INTEGRATION_ADDRESS:-${DODO_PMM_INTEGRATION:-}}" +PRIVATE_KEY="${PRIVATE_KEY:-}" + +command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 1; } +command -v cast >/dev/null 2>&1 || { echo "cast is required" >&2; exit 1; } + +[[ -f "$PLAN_JSON" ]] || { echo "PLAN_JSON not found: $PLAN_JSON" >&2; exit 1; } +[[ -n "$DODO_PMM_INTEGRATION_ADDRESS" ]] || { echo "DODO_PMM_INTEGRATION_ADDRESS not set" >&2; exit 1; } +require_private_key_env || exit 1 + +EXECUTION_CONFIG="$(jq -r '.executionConfig' "$PLAN_JSON")" +if [[ "$EXECUTION_CONFIG" != /* ]]; then + EXECUTION_CONFIG="$SMOM_ROOT/${EXECUTION_CONFIG#smom-dbis-138/}" +fi +[[ -f "$EXECUTION_CONFIG" ]] || { echo "Execution config not found: $EXECUTION_CONFIG" >&2; exit 1; } + +if [[ -z "$PROFILE" ]]; then + PROFILE="$(jq -r '.defaultProfile' "$PLAN_JSON")" +fi + +FIAT_BASE_UNITS="$(jq -r --arg p "$PROFILE" '.profiles[$p].fiatBaseUnits' "$PLAN_JSON")" +XAU_BASE_UNITS="$(jq -r --arg p "$PROFILE" '.profiles[$p].xauBaseUnits' "$PLAN_JSON")" +[[ "$FIAT_BASE_UNITS" != "null" ]] || { echo "Unknown PROFILE: $PROFILE" >&2; exit 1; } +[[ "$XAU_BASE_UNITS" != "null" ]] || { echo "Unknown PROFILE: $PROFILE" >&2; exit 1; } + +APPROVE_GAS_LIMIT="$(jq -r '.execution.approveGasLimit' "$PLAN_JSON")" +MINT_GAS_LIMIT="$(jq -r '.execution.mintGasLimit' "$PLAN_JSON")" +WRAP_GAS_LIMIT="$(jq -r '.execution.wrapGasLimit' "$PLAN_JSON")" +ADD_LIQUIDITY_GAS_LIMIT="$(jq -r '.execution.addLiquidityGasLimit' "$PLAN_JSON")" +GAS_PRICE_WEI="${CHAIN_GAS_PRICE:-$(jq -r '.execution.legacyGasPriceWei' "$PLAN_JSON")}" +WRAP_WHEN_NEEDED="$(jq -r '.execution.wrapNativeEthWhenNeeded' "$PLAN_JSON")" +MINT_WHEN_OWNER="$(jq -r '.execution.mintMissingBaseWhenOwner' "$PLAN_JSON")" +APPROVE_MAX="$(jq -r '.execution.approveMax' "$PLAN_JSON")" + +DEPLOYER="$(derive_deployer_address || true)" +[[ -n "$DEPLOYER" ]] || { echo "ERROR: Could not derive DEPLOYER_ADDRESS from PRIVATE_KEY." >&2; exit 1; } +WETH_ADDRESS="$(jq -r '.tokens.WETH' "$EXECUTION_CONFIG")" +MAX_UINT="0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +ZERO_ADDR="0x0000000000000000000000000000000000000000" + +log() { + printf '%s\n' "$*" +} + +get_pool_reserves() { + local pool="$1" + local lines + mapfile -t lines < <(cast call "$pool" 'getVaultReserve()(uint256,uint256)' --rpc-url "$RPC_URL_138") + printf '%s\t%s\n' "$(uint_clean "${lines[0]:-0}")" "$(uint_clean "${lines[1]:-0}")" +} + +uint_clean() { + printf '%s\n' "$1" | awk '{print $1}' +} + +hex_to_addr() { + local raw="${1#0x}" + if [[ ${#raw} -lt 40 ]]; then + printf '%s\n' "$ZERO_ADDR" + return 0 + fi + printf '0x%s\n' "${raw: -40}" +} + +send_tx() { + local to="$1" + shift + cast send "$to" "$@" \ + --rpc-url "$RPC_URL_138" \ + --private-key "$PRIVATE_KEY" \ + --legacy \ + --gas-price "$GAS_PRICE_WEI" \ + -q +} + +big_add() { + python3 - "$1" "$2" <<'PY' +import sys +print(int(sys.argv[1]) + int(sys.argv[2])) +PY +} + +big_sub_if_positive() { + python3 - "$1" "$2" <<'PY' +import sys +a = int(sys.argv[1]) +b = int(sys.argv[2]) +print(a - b if a > b else 0) +PY +} + +big_lt() { + python3 - "$1" "$2" <<'PY' +import sys +print("1" if int(sys.argv[1]) < int(sys.argv[2]) else "0") +PY +} + +raw_to_human_6() { + awk -v v="$1" 'BEGIN { printf "%.6f", v / 1000000 }' +} + +raw_to_human_18() { + awk -v v="$1" 'BEGIN { printf "%.18f", v / 1000000000000000000 }' +} + +mul_div_round() { + awk -v a="$1" -v b="$2" -v c="$3" 'BEGIN { printf "%.0f", (a * b) / c }' +} + +derive_usd_per_eth() { + local pool base_reserve quote_reserve + local cusdt quote + cusdt="$(jq -r '.tokens.cUSDT' "$EXECUTION_CONFIG")" + quote="$WETH_ADDRESS" + pool="$(hex_to_addr "$(cast call "$DODO_PMM_INTEGRATION_ADDRESS" 'pools(address,address)(address)' "$cusdt" "$quote" --rpc-url "$RPC_URL_138")")" + [[ "$pool" != "$ZERO_ADDR" ]] || { echo "0"; return 1; } + read -r base_reserve quote_reserve < <(get_pool_reserves "$pool") + awk -v base="$base_reserve" -v quote="$quote_reserve" 'BEGIN { printf "%.12f", (base / 1000000) / (quote / 1000000000000000000) }' +} + +derive_xau_usd_per_unit() { + local pool base_symbol quote_symbol base_addr quote_addr base_reserve quote_reserve fallback + base_symbol="$(jq -r '.xauPricing.poolBaseSymbol' "$PLAN_JSON")" + quote_symbol="$(jq -r '.xauPricing.poolQuoteSymbol' "$PLAN_JSON")" + fallback="$(jq -r '.xauPricing.fallbackUsdPerUnit' "$PLAN_JSON")" + base_addr="$(jq -r --arg sym "$base_symbol" '.tokens[$sym]' "$EXECUTION_CONFIG")" + quote_addr="$(jq -r --arg sym "$quote_symbol" '.tokens[$sym]' "$EXECUTION_CONFIG")" + pool="$(hex_to_addr "$(cast call "$DODO_PMM_INTEGRATION_ADDRESS" 'pools(address,address)(address)' "$base_addr" "$quote_addr" --rpc-url "$RPC_URL_138")")" + if [[ "$pool" == "$ZERO_ADDR" ]]; then + echo "$fallback" + return 0 + fi + read -r base_reserve quote_reserve < <(get_pool_reserves "$pool") + if [[ "$base_reserve" == "0" || "$quote_reserve" == "0" ]]; then + echo "$fallback" + return 0 + fi + awk -v base="$base_reserve" -v quote="$quote_reserve" 'BEGIN { printf "%.12f", (quote / 1000000) / (base / 1000000) }' +} + +usd_per_eth="$(derive_usd_per_eth)" +[[ "$usd_per_eth" != "0" ]] || { echo "Could not derive USD per ETH from live cUSDT/WETH pool" >&2; exit 1; } +xau_usd_per_unit="$(derive_xau_usd_per_unit)" + +log "=== Chain 138 ETH PMM Liquidity Funding ===" +log "Mode: $( [[ "$EXECUTE" == "1" ]] && echo EXECUTE || echo DRY_RUN )" +log "Profile: $PROFILE" +log "Plan: $PLAN_JSON" +log "Config: $EXECUTION_CONFIG" +log "Integration: $DODO_PMM_INTEGRATION_ADDRESS" +log "RPC: $RPC_URL_138" +log "Deployer: $DEPLOYER" +log "USD per ETH: $usd_per_eth" +log "XAU USD: $xau_usd_per_unit" +log "" + +total_quote_units=0 +wrap_needed=0 + +declare -a ROWS=() +while IFS=$'\t' read -r base_symbol base_address; do + pool="$(hex_to_addr "$(cast call "$DODO_PMM_INTEGRATION_ADDRESS" 'pools(address,address)(address)' "$base_address" "$WETH_ADDRESS" --rpc-url "$RPC_URL_138")")" + [[ "$pool" != "$ZERO_ADDR" ]] || continue + + case "$base_symbol" in + cXAUC|cXAUT) + base_units="$XAU_BASE_UNITS" + usd_per_unit="$xau_usd_per_unit" + ;; + *) + base_units="$FIAT_BASE_UNITS" + usd_per_unit="$(jq -r --arg sym "$base_symbol" '.usdPerUnit[$sym]' "$PLAN_JSON")" + [[ "$usd_per_unit" != "null" ]] || { echo "Missing usdPerUnit for $base_symbol" >&2; exit 1; } + ;; + esac + + quote_units="$(awk -v base="$base_units" -v usd="$usd_per_unit" -v usd_eth="$usd_per_eth" 'BEGIN { printf "%.0f", ((base / 1000000) * usd / usd_eth) * 1000000000000000000 }')" + read -r current_base_reserve current_quote_reserve < <(get_pool_reserves "$pool") + base_delta="$(big_sub_if_positive "$base_units" "$current_base_reserve")" + quote_delta="$(big_sub_if_positive "$quote_units" "$current_quote_reserve")" + base_balance="$(cast call "$base_address" 'balanceOf(address)(uint256)' "$DEPLOYER" --rpc-url "$RPC_URL_138" 2>/dev/null || echo 0)" + base_balance="$(uint_clean "$base_balance")" + owner_addr="$(cast call "$base_address" 'owner()(address)' --rpc-url "$RPC_URL_138" 2>/dev/null || echo "$ZERO_ADDR")" + ROWS+=("$base_symbol|$base_address|$pool|$base_units|$quote_units|$current_base_reserve|$current_quote_reserve|$base_delta|$quote_delta|$base_balance|$owner_addr") + total_quote_units="$(big_add "$total_quote_units" "$quote_delta")" +done < <(jq -r '.explicitPairs[] | [.baseSymbol, (.baseSymbol as $s | .baseSymbol)] | @tsv' "$EXECUTION_CONFIG" | while IFS=$'\t' read -r base_symbol _; do + base_address="$(jq -r --arg sym "$base_symbol" '.tokens[$sym]' "$EXECUTION_CONFIG")" + printf '%s\t%s\n' "$base_symbol" "$base_address" +done) + +weth_balance="$(cast call "$WETH_ADDRESS" 'balanceOf(address)(uint256)' "$DEPLOYER" --rpc-url "$RPC_URL_138" 2>/dev/null || echo 0)" +weth_balance="$(uint_clean "$weth_balance")" +eth_balance="$(cast balance "$DEPLOYER" --rpc-url "$RPC_URL_138" 2>/dev/null || echo 0)" +if [[ "$(big_lt "$weth_balance" "$total_quote_units")" == "1" ]]; then + wrap_needed="$(big_sub_if_positive "$total_quote_units" "$weth_balance")" +fi + +log "Current ETH: $(raw_to_human_18 "$eth_balance")" +log "Current WETH: $(raw_to_human_18 "$weth_balance")" +log "Total WETH needed for profile: $(raw_to_human_18 "$total_quote_units")" +log "Additional WETH to wrap: $(raw_to_human_18 "$wrap_needed")" +log "" + +for row in "${ROWS[@]}"; do + IFS='|' read -r base_symbol base_address pool base_units quote_units current_base_reserve current_quote_reserve base_delta quote_delta base_balance owner_addr <<< "$row" + need_mint=0 + if [[ "$(big_lt "$base_balance" "$base_delta")" == "1" ]]; then + need_mint="$(big_sub_if_positive "$base_delta" "$base_balance")" + fi + log "$base_symbol pool=$pool" + log " target base: $(raw_to_human_6 "$base_units")" + log " target quote: $(raw_to_human_18 "$quote_units") WETH" + log " current base: $(raw_to_human_6 "$current_base_reserve")" + log " current quote: $(raw_to_human_18 "$current_quote_reserve") WETH" + log " top-up base: $(raw_to_human_6 "$base_delta")" + log " top-up quote: $(raw_to_human_18 "$quote_delta") WETH" + log " base balance: $(raw_to_human_6 "$base_balance")" + if [[ "$need_mint" != "0" ]]; then + log " mint needed: $(raw_to_human_6 "$need_mint")" + else + log " mint needed: no" + fi +done + +if [[ "$EXECUTE" != "1" ]]; then + log "" + log "Dry run only. Re-run with EXECUTE=1 to mint/wrap/approve/add liquidity." + exit 0 +fi + +if [[ "$wrap_needed" != "0" ]]; then + if [[ "$WRAP_WHEN_NEEDED" != "true" ]]; then + echo "Need additional WETH but wrapNativeEthWhenNeeded=false" >&2 + exit 1 + fi + if [[ "$(big_lt "$eth_balance" "$wrap_needed")" == "1" ]]; then + echo "Insufficient ETH to wrap required WETH" >&2 + exit 1 + fi + log "Wrapping $(raw_to_human_18 "$wrap_needed") ETH into WETH" + send_tx "$WETH_ADDRESS" 'deposit()' --value "$wrap_needed" --gas-limit "$WRAP_GAS_LIMIT" >/dev/null +fi + +for row in "${ROWS[@]}"; do + IFS='|' read -r base_symbol base_address pool base_units quote_units current_base_reserve current_quote_reserve base_delta quote_delta base_balance owner_addr <<< "$row" + if [[ "$base_delta" == "0" && "$quote_delta" == "0" ]]; then + log "Skipping $base_symbol/WETH; already at or above target." + continue + fi + + if [[ "$(big_lt "$base_balance" "$base_delta")" == "1" ]]; then + mint_amount="$(big_sub_if_positive "$base_delta" "$base_balance")" + if [[ "$MINT_WHEN_OWNER" != "true" ]]; then + echo "Need to mint $base_symbol but mintMissingBaseWhenOwner=false" >&2 + exit 1 + fi + if [[ "${owner_addr,,}" != "${DEPLOYER,,}" ]]; then + echo "Cannot mint $base_symbol; deployer is not owner ($owner_addr)" >&2 + exit 1 + fi + log "Minting $(raw_to_human_6 "$mint_amount") $base_symbol" + send_tx "$base_address" 'mint(address,uint256)' "$DEPLOYER" "$mint_amount" --gas-limit "$MINT_GAS_LIMIT" >/dev/null + fi + + if [[ "$APPROVE_MAX" == "true" ]]; then + log "Approving $base_symbol to integration" + send_tx "$base_address" 'approve(address,uint256)' "$DODO_PMM_INTEGRATION_ADDRESS" "$MAX_UINT" --gas-limit "$APPROVE_GAS_LIMIT" >/dev/null + log "Approving WETH for $base_symbol pair" + send_tx "$WETH_ADDRESS" 'approve(address,uint256)' "$DODO_PMM_INTEGRATION_ADDRESS" "$MAX_UINT" --gas-limit "$APPROVE_GAS_LIMIT" >/dev/null + fi + + log "Adding liquidity to $base_symbol/WETH at $pool" + send_tx "$DODO_PMM_INTEGRATION_ADDRESS" 'addLiquidity(address,uint256,uint256)' "$pool" "$base_delta" "$quote_delta" --gas-limit "$ADD_LIQUIDITY_GAS_LIMIT" >/dev/null +done + +log "" +log "Liquidity funding complete." diff --git a/scripts/deployment/fund-uniswap-v3-gas-pool.sh b/scripts/deployment/fund-uniswap-v3-gas-pool.sh new file mode 100755 index 0000000..2ebec51 --- /dev/null +++ b/scripts/deployment/fund-uniswap-v3-gas-pool.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "$REPO_ROOT" +fi + +POSITION_MANAGER="${POSITION_MANAGER:-}" +RPC_URL="${RPC_URL:-}" +TOKEN_A="${TOKEN_A:-}" +TOKEN_B="${TOKEN_B:-}" +AMOUNT_A="${AMOUNT_A:-}" +AMOUNT_B="${AMOUNT_B:-}" +FEE="${FEE:-500}" +EXECUTE="${EXECUTE:-0}" +DEADLINE_SECONDS="${DEADLINE_SECONDS:-3600}" +RECIPIENT="${RECIPIENT:-}" +PRIVATE_KEY="${PRIVATE_KEY:-}" + +if [[ -z "$POSITION_MANAGER" || -z "$RPC_URL" || -z "$TOKEN_A" || -z "$TOKEN_B" || -z "$AMOUNT_A" || -z "$AMOUNT_B" ]]; then + echo "Required: POSITION_MANAGER RPC_URL TOKEN_A TOKEN_B AMOUNT_A AMOUNT_B" >&2 + exit 1 +fi + +if [[ -z "$RECIPIENT" ]]; then + if [[ -z "$PRIVATE_KEY" ]]; then + echo "Set RECIPIENT or PRIVATE_KEY" >&2 + exit 1 + fi + RECIPIENT="$(cast wallet address --private-key "$PRIVATE_KEY")" +fi + +lower_hex="$(printf '%s\n%s\n' "$TOKEN_A" "$TOKEN_B" | tr '[:upper:]' '[:lower:]' | sort | sed -n '1p')" +upper_hex="$(printf '%s\n%s\n' "$TOKEN_A" "$TOKEN_B" | tr '[:upper:]' '[:lower:]' | sort | sed -n '2p')" + +token0="$lower_hex" +token1="$upper_hex" +amount0="$AMOUNT_A" +amount1="$AMOUNT_B" + +if [[ "${TOKEN_A,,}" != "$token0" ]]; then + amount0="$AMOUNT_B" + amount1="$AMOUNT_A" +fi + +case "$FEE" in + 100) tick_spacing=1 ;; + 500) tick_spacing=10 ;; + 3000) tick_spacing=60 ;; + 10000) tick_spacing=200 ;; + *) + echo "Unsupported fee tier for tick-spacing inference: $FEE" >&2 + exit 1 + ;; +esac + +min_tick=-887272 +max_tick=887272 +tick_lower=$(( (min_tick / tick_spacing) * tick_spacing )) +tick_upper=$(( (max_tick / tick_spacing) * tick_spacing )) + +deadline="$(($(date +%s) + DEADLINE_SECONDS))" + +echo "Funding Uniswap v3 pool" +echo " token0: $token0 amount0: $amount0" +echo " token1: $token1 amount1: $amount1" +echo " fee: $FEE ticks: [$tick_lower, $tick_upper]" +echo " recipient: $RECIPIENT" + +if [[ "$EXECUTE" != "1" ]]; then + echo "Dry run only." + echo "Approve token0:" + echo " cast send \"$token0\" 'approve(address,uint256)' \"$POSITION_MANAGER\" \"$amount0\" --rpc-url \"$RPC_URL\" --private-key \"\$PRIVATE_KEY\"" + echo "Approve token1:" + echo " cast send \"$token1\" 'approve(address,uint256)' \"$POSITION_MANAGER\" \"$amount1\" --rpc-url \"$RPC_URL\" --private-key \"\$PRIVATE_KEY\"" + echo "Mint position:" + echo " cast send \"$POSITION_MANAGER\" \"mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))\" \"($token0,$token1,$FEE,$tick_lower,$tick_upper,$amount0,$amount1,0,0,$RECIPIENT,$deadline)\" --rpc-url \"$RPC_URL\" --private-key \"\$PRIVATE_KEY\" -vv" + exit 0 +fi + +if [[ -z "$PRIVATE_KEY" ]]; then + echo "PRIVATE_KEY is required when EXECUTE=1" >&2 + exit 1 +fi + +cast send "$token0" \ + "approve(address,uint256)" \ + "$POSITION_MANAGER" \ + "$amount0" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + -q + +cast send "$token1" \ + "approve(address,uint256)" \ + "$POSITION_MANAGER" \ + "$amount1" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + -q + +cast send "$POSITION_MANAGER" \ + "mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))" \ + "($token0,$token1,$FEE,$tick_lower,$tick_upper,$amount0,$amount1,0,0,$RECIPIENT,$deadline)" \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + -vv diff --git a/scripts/deployment/verify-all-networks-explorers.sh b/scripts/deployment/verify-all-networks-explorers.sh new file mode 100755 index 0000000..6f3b509 --- /dev/null +++ b/scripts/deployment/verify-all-networks-explorers.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Run all explorer verification entrypoints: Chain 138 Blockscout, Ethereum, multichain cW*, +# Avax/Arb bridges, optional Cronos CCIP, optional CCIPLogger (Hardhat), optional Wemix. +# Steps are best-effort (continue on failure); check log for FAILED lines. +# +# Usage: cd smom-dbis-138 && ./scripts/deployment/verify-all-networks-explorers.sh +set -uo pipefail +set +e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SMOM="$(cd "$SCRIPT_DIR/../.." && pwd)" +# Parent of smom-dbis-138 = proxmox workspace root +PROXMOX="$(cd "$SMOM/.." && pwd)" + +STEPS_FAILED=0 +step() { + local title="$1" + shift + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "$title" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + if "$@"; then + echo "[ok] $title" + else + echo "[fail] $title (exit $?)" >&2 + STEPS_FAILED=$((STEPS_FAILED + 1)) + fi +} + +# Load env for keys +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "${SMOM}" +fi + +if [[ -f "$PROXMOX/scripts/verify/run-contract-verification-with-proxy.sh" ]]; then + step "Chain 138 Blockscout (forge proxy)" bash "$PROXMOX/scripts/verify/run-contract-verification-with-proxy.sh" || true +else + echo "WARN: Proxmox verify proxy not found at $PROXMOX/scripts/verify/" >&2 +fi + +step "Ethereum Mainnet CCIP bridges" bash "$SCRIPT_DIR/verify-mainnet-etherscan.sh" || true +step "Ethereum Mainnet cW* (CompliantWrappedToken)" bash "$SCRIPT_DIR/verify-mainnet-cw-etherscan.sh" || true +step "Multichain cW* (all CW*_* except MAINNET)" bash "$SCRIPT_DIR/verify-multichain-cw-etherscan.sh" || true +step "Avalanche + Arbitrum WETH/CCIP bridges" bash "$SCRIPT_DIR/verify-deployed-contracts.sh" || true + +# Bytecode + manual-verify instructions (no explorer API key required). +step "Cronos deployments + manual verify runbook" bash "$SCRIPT_DIR/verify-cronos-contracts.sh" || true + +if command -v npx &>/dev/null && [[ -f "$SMOM/hardhat.config.ts" || -f "$SMOM/hardhat.config.js" ]]; then + step "CCIPLogger (Hardhat verify, multichain)" bash "$SCRIPT_DIR/verify-ccip-logger-other-chains.sh" || true +else + echo "Skip CCIPLogger: npx/hardhat config not available" +fi + +if [[ -n "${WEMIXSCAN_API_KEY:-}" && -n "${WEMIX_RPC:-}" && -n "${CCIPWETH9_BRIDGE_WEMIX:-}" ]]; then + step "Wemix CCIP bridges" bash "$SCRIPT_DIR/verify-wemix-bridges.sh" || true +else + echo "Skip Wemix: WEMIXSCAN_API_KEY / WEMIX_RPC / CCIPWETH9_BRIDGE_WEMIX not all set" +fi + +echo "" +echo "=== verify-all-networks-explorers finished: step failures=$STEPS_FAILED ===" +exit 0 diff --git a/scripts/deployment/verify-mainnet-cw-etherscan.sh b/scripts/deployment/verify-mainnet-cw-etherscan.sh new file mode 100755 index 0000000..a6b0d39 --- /dev/null +++ b/scripts/deployment/verify-mainnet-cw-etherscan.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Verify Ethereum Mainnet CompliantWrappedToken (cWUSDT…cWXAUT) on Etherscan. +# Constructor matches script/deploy/DeployCWTokens.s.sol (name, symbol, 6 decimals, deployer admin). +# +# Requires: ETHERSCAN_API_KEY, and either CW_VERIFY_ADMIN=0x... (deployer used at deploy) or PRIVATE_KEY +# (address derived via cast wallet address) to ABI-encode constructor args. +# +# Usage: +# cd smom-dbis-138 && ./scripts/deployment/verify-mainnet-cw-etherscan.sh +# ./scripts/deployment/verify-mainnet-cw-etherscan.sh --dry-run # print forge commands only +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$PROJECT_ROOT" + +DRY_RUN=false +for a in "$@"; do + case "$a" in + --dry-run) DRY_RUN=true ;; + esac +done + +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "${PROJECT_ROOT}" +elif [[ -f "$PROJECT_ROOT/.env" ]]; then + set -a + # shellcheck disable=SC1090 + source "$PROJECT_ROOT/.env" + set +a +fi + +if [[ -z "${ETHERSCAN_API_KEY:-}" ]]; then + echo "ETHERSCAN_API_KEY not set (see https://etherscan.io/myapikey)" >&2 + exit 1 +fi + +ADMIN="${CW_VERIFY_ADMIN:-}" +if [[ -z "$ADMIN" && -n "${PRIVATE_KEY:-}" ]]; then + ADMIN="$(cast wallet address --private-key "$PRIVATE_KEY")" +fi +if [[ -z "$ADMIN" || "$ADMIN" == "0x0000000000000000000000000000000000000000" ]]; then + echo "Set CW_VERIFY_ADMIN to the deployer address used in CompliantWrappedToken constructor, or set PRIVATE_KEY." >&2 + exit 1 +fi + +verify_one() { + local envkey="$1" cname="$2" sym="$3" + local addr + addr="${!envkey:-}" + if [[ -z "$addr" ]]; then + echo " skip $sym: $envkey unset" + return 0 + fi + local enc + enc="$(cast abi-encode "constructor(string,string,uint8,address)" "$cname" "$sym" 6 "$ADMIN")" + if $DRY_RUN; then + echo "forge verify-contract --chain-id 1 --num-of-optimizations 200 --via-ir --constructor-args \"$enc\" --etherscan-api-key \"\$ETHERSCAN_API_KEY\" \"$addr\" contracts/tokens/CompliantWrappedToken.sol:CompliantWrappedToken" + return 0 + fi + set +e + out="$(forge verify-contract \ + --chain-id 1 \ + --num-of-optimizations 200 \ + --via-ir \ + --constructor-args "$enc" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" \ + "$addr" \ + "contracts/tokens/CompliantWrappedToken.sol:CompliantWrappedToken" 2>&1)" + rc=$? + set -e + if [[ $rc -eq 0 ]]; then + echo " ok $sym $addr" + elif echo "$out" | grep -qiE 'already verified|Already Verified'; then + echo " already verified $sym $addr" + else + echo " FAILED $sym $addr (exit $rc)" >&2 + echo "$out" >&2 + return "$rc" + fi +} + +echo "=== Verify Mainnet CompliantWrappedToken (admin=$ADMIN) ===" +# envVar|constructor name|symbol — must match DeployCWTokens.s.sol +while IFS='|' read -r envkey cname sym; do + [[ -z "$envkey" ]] && continue + verify_one "$envkey" "$cname" "$sym" +done <<'EOF' +CWUSDT_MAINNET|Wrapped cUSDT|cWUSDT +CWUSDC_MAINNET|Wrapped cUSDC|cWUSDC +CWEURC_MAINNET|Wrapped cEURC|cWEURC +CWEURT_MAINNET|Wrapped cEURT|cWEURT +CWGBPC_MAINNET|Wrapped cGBPC|cWGBPC +CWGBPT_MAINNET|Wrapped cGBPT|cWGBPT +CWAUDC_MAINNET|Wrapped cAUDC|cWAUDC +CWJPYC_MAINNET|Wrapped cJPYC|cWJPYC +CWCHFC_MAINNET|Wrapped cCHFC|cWCHFC +CWCADC_MAINNET|Wrapped cCADC|cWCADC +CWXAUC_MAINNET|Wrapped cXAUC|cWXAUC +CWXAUT_MAINNET|Wrapped cXAUT|cWXAUT +EOF + +echo "Done." diff --git a/scripts/deployment/verify-multichain-cw-etherscan.sh b/scripts/deployment/verify-multichain-cw-etherscan.sh new file mode 100755 index 0000000..ff6ca81 --- /dev/null +++ b/scripts/deployment/verify-multichain-cw-etherscan.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Verify CompliantWrappedToken (cW*) on all chains where addresses exist in .env. +# Skips CW*_MAINNET (use verify-mainnet-cw-etherscan.sh). Same constructor as DeployCWTokens. +# +# Requires: ETHERSCAN_API_KEY (Etherscan v2 / unified explorers); CRONOSCAN_API_KEY for Cronos. +# CW_VERIFY_ADMIN or PRIVATE_KEY for constructor admin address. +# +# Usage: cd smom-dbis-138 && ./scripts/deployment/verify-multichain-cw-etherscan.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$PROJECT_ROOT" + +if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then + # shellcheck disable=SC1090 + source "$SCRIPT_DIR/../lib/deployment/dotenv.sh" + load_deployment_env --repo-root "${PROJECT_ROOT}" +elif [[ -f "$PROJECT_ROOT/.env" ]]; then + set -a + # shellcheck disable=SC1090 + source "$PROJECT_ROOT/.env" + set +a +fi + +if [[ -z "${ETHERSCAN_API_KEY:-}" ]]; then + echo "ETHERSCAN_API_KEY not set" >&2 + exit 1 +fi + +ADMIN="${CW_VERIFY_ADMIN:-}" +if [[ -z "$ADMIN" && -n "${PRIVATE_KEY:-}" ]]; then + ADMIN="$(cast wallet address --private-key "$PRIVATE_KEY")" +fi +if [[ -z "$ADMIN" || "$ADMIN" == "0x0000000000000000000000000000000000000000" ]]; then + echo "Set CW_VERIFY_ADMIN or PRIVATE_KEY" >&2 + exit 1 +fi + +# Uppercase chain suffix -> forge --chain name (Foundry) +forge_chain() { + case "$1" in + BSC) echo bsc ;; + POLYGON) echo polygon ;; + GNOSIS) echo gnosis ;; + OPTIMISM) echo optimism ;; + BASE) echo base ;; + ARBITRUM) echo arbitrum ;; + AVALANCHE) echo avalanche ;; + CELO) echo celo ;; + CRONOS) echo cronos ;; + *) echo "" ;; + esac +} + +# API key for forge (Cronos uses cronoscan) +api_key_for() { + case "$1" in + CRONOS) echo "${CRONOSCAN_API_KEY:-}" ;; + *) echo "${ETHERSCAN_API_KEY}" ;; + esac +} + +# cWUSDT -> Wrapped cUSDT / cWUSDT +name_sym_from_cw() { + local cw="$1" + local core="${cw#CW}" + echo "Wrapped c${core}|cW${core}" +} + +OK=0 +SKIP=0 +FAIL=0 + +while IFS= read -r line; do + [[ "$line" =~ ^CW[A-Z0-9]+_[A-Z0-9]+= ]] || continue + key="${line%%=*}" + addr="${line#*=}" + [[ -z "$addr" || "$addr" == "0x0000000000000000000000000000000000000000" ]] && continue + chain="${key##*_}" + [[ "$chain" == "MAINNET" ]] && continue + # Cronos: Foundry + Etherscan API v2 often errors ("chainid"); use verify-cronos-contracts.sh or explorer.cronos.org manual upload. + [[ "$chain" == "CRONOS" ]] && { echo " skip $key (Cronos: use verify-cronos-contracts.sh or CRONOS_VERIFICATION_RUNBOOK)"; SKIP=$((SKIP + 1)); continue; } + + fc="$(forge_chain "$chain")" + if [[ -z "$fc" ]]; then + echo " skip $key (unknown chain suffix $chain)" + SKIP=$((SKIP + 1)) + continue + fi + + api="$(api_key_for "$chain")" + if [[ -z "$api" ]]; then + echo " skip $key (no API key for $chain; set CRONOSCAN_API_KEY for Cronos)" >&2 + SKIP=$((SKIP + 1)) + continue + fi + + cwpart="${key%_*}" + IFS='|' read -r cname sym <<< "$(name_sym_from_cw "$cwpart")" + enc="$(cast abi-encode "constructor(string,string,uint8,address)" "$cname" "$sym" 6 "$ADMIN")" + + echo "[$fc] $sym $addr ..." + set +e + out="$(forge verify-contract \ + --chain "$fc" \ + --num-of-optimizations 200 \ + --via-ir \ + --constructor-args "$enc" \ + --etherscan-api-key "$api" \ + "$addr" \ + "contracts/tokens/CompliantWrappedToken.sol:CompliantWrappedToken" 2>&1)" + rc=$? + set -e + if [[ $rc -eq 0 ]]; then + echo " ok" + OK=$((OK + 1)) + elif echo "$out" | grep -qiE 'already verified|Already Verified|Contract source code already verified'; then + echo " already verified" + OK=$((OK + 1)) + else + echo " FAILED (exit $rc)" >&2 + echo "$out" >&2 + FAIL=$((FAIL + 1)) + fi + sleep 1 +done < <(env | grep '^CW[A-Z0-9]*_' | sort) + +echo "" +echo "=== multichain cW*: ok/already=$OK skipped=$SKIP failed=$FAIL ===" diff --git a/scripts/hybx-omnl/ci-omnl-validation.sh b/scripts/hybx-omnl/ci-omnl-validation.sh new file mode 100755 index 0000000..a919e15 --- /dev/null +++ b/scripts/hybx-omnl/ci-omnl-validation.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" +bash scripts/hybx-omnl/verify-deployment.sh +echo "== omnl reconcile artifact ==" +bash scripts/hybx-omnl/omnl-reconcile-artifact.sh +cd services/token-aggregation +if command -v pnpm >/dev/null 2>&1; then + pnpm run build +else + npm run build +fi +echo "ci-omnl-validation: OK" diff --git a/scripts/hybx-omnl/omnl-reconcile-artifact.sh b/scripts/hybx-omnl/omnl-reconcile-artifact.sh new file mode 100755 index 0000000..bdb3252 --- /dev/null +++ b/scripts/hybx-omnl/omnl-reconcile-artifact.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Run OMNL IPSAS + journal-matrix anchor and persist JSON + sha256 under artifacts/omnl-reconcile/. +# Usage: from repo root — bash scripts/hybx-omnl/omnl-reconcile-artifact.sh +# Env: +# OMNL_RECONCILE_ARTIFACT_DIR — output directory (default: $REPO_ROOT/artifacts/omnl-reconcile) +# OMNL_IPSAS_GL_REGISTRY, OMNL_JOURNAL_MATRIX_PATH — passed through to omnl-reconcile-report.mjs +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +OUT="${OMNL_RECONCILE_ARTIFACT_DIR:-$ROOT/artifacts/omnl-reconcile}" +TS="$(date -u +%Y%m%dT%H%M%SZ)" + +mkdir -p "$OUT" +# Invoke the report script with node so stdout is JSON only (npm run adds lifecycle lines). +node "$ROOT/services/token-aggregation/scripts/omnl-reconcile-report.mjs" > "$OUT/omnl-reconcile-${TS}.json" +cp -f "$OUT/omnl-reconcile-${TS}.json" "$OUT/omnl-reconcile-latest.json" + +node -e " +const fs = require('fs'); +const j = JSON.parse(fs.readFileSync('$OUT/omnl-reconcile-latest.json', 'utf8')); +fs.writeFileSync('$OUT/omnl-reconcile-sha256.txt', String(j.sha256).trim() + '\n'); +" + +if [[ -n "${GITHUB_SHA:-}" ]] || [[ -n "${GITHUB_RUN_ID:-}" ]]; then + node -e " +const fs = require('fs'); +const o = { + generatedAt: new Date().toISOString(), + githubSha: process.env.GITHUB_SHA || null, + runId: process.env.GITHUB_RUN_ID || null, + repository: process.env.GITHUB_REPOSITORY || null, +}; +fs.writeFileSync('$OUT/omnl-reconcile-ci-meta.json', JSON.stringify(o, null, 2) + '\n'); +" 2>/dev/null || true +fi + +echo "OMNL reconcile artifacts written:" +echo " $OUT/omnl-reconcile-${TS}.json" +echo " $OUT/omnl-reconcile-latest.json" +echo " $OUT/omnl-reconcile-sha256.txt ($(cat "$OUT/omnl-reconcile-sha256.txt" | tr -d '\n'))" diff --git a/scripts/hybx-omnl/sync-to-publish.sh b/scripts/hybx-omnl/sync-to-publish.sh new file mode 100755 index 0000000..be03094 --- /dev/null +++ b/scripts/hybx-omnl/sync-to-publish.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Copy HYBX OMNL–related paths from smom-dbis-138 → smom-dbis-138-publish (adjust PUBLISH_ROOT if needed). +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PUBLISH_ROOT="${PUBLISH_ROOT:-$ROOT/../smom-dbis-138-publish}" +if [[ ! -d "$PUBLISH_ROOT" ]]; then + echo "error: PUBLISH_ROOT not found: $PUBLISH_ROOT" >&2 + exit 1 +fi + +rsync -a "$ROOT/contracts/hybx-omnl/" "$PUBLISH_ROOT/contracts/hybx-omnl/" +rsync -a "$ROOT/script/hybx-omnl/" "$PUBLISH_ROOT/script/hybx-omnl/" +rsync -a "$ROOT/test/hybx-omnl/" "$PUBLISH_ROOT/test/hybx-omnl/" +rsync -a "$ROOT/docs/hybx-omnl/" "$PUBLISH_ROOT/docs/hybx-omnl/" +for f in hybx-omnl-policy.json hybx-omnl-cross-chain-lines.json omnl-ipsas-gl-registry.json omnl-journal-matrix.json deployment-omnl.example.env; do + [[ -f "$ROOT/config/$f" ]] && cp -f "$ROOT/config/$f" "$PUBLISH_ROOT/config/$f" +done +rsync -a "$ROOT/scripts/hybx-omnl/" "$PUBLISH_ROOT/scripts/hybx-omnl/" +rsync -a "$ROOT/scripts/forge/scope.sh" "$PUBLISH_ROOT/scripts/forge/scope.sh" +mkdir -p "$PUBLISH_ROOT/.github/workflows" +for w in hybx-omnl-ts.yml omnl-reconcile.yml; do + [[ -f "$ROOT/.github/workflows/$w" ]] && cp -f "$ROOT/.github/workflows/$w" "$PUBLISH_ROOT/.github/workflows/$w" +done + +TA="$ROOT/services/token-aggregation" +PTA="$PUBLISH_ROOT/services/token-aggregation" +for f in omnl-webhooks.ts omnl-ipsas-gl.ts omnl-journal-matrix.ts omnl-compliance.ts omnl-policy-math.ts omnl-reconcile-anchor.ts omnl-integration-status.ts omnl-api-catalog.ts; do + cp -f "$TA/src/services/$f" "$PTA/src/services/$f" +done +cp -f "$TA/src/services/omnl-reconcile-anchor.test.ts" "$PTA/src/services/omnl-reconcile-anchor.test.ts" 2>/dev/null || true +cp -f "$TA/src/api/routes/omnl.ts" "$PTA/src/api/routes/omnl.ts" +cp -f "$TA/src/api/routes/omnl-ipsas.ts" "$PTA/src/api/routes/omnl-ipsas.ts" +cp -f "$TA/src/indexer/omnl-event-poller.ts" "$PTA/src/indexer/omnl-event-poller.ts" +cp -f "$TA/src/indexer/omnl-poller-state.ts" "$PTA/src/indexer/omnl-poller-state.ts" +cp -f "$TA/src/api/server.ts" "$PTA/src/api/server.ts" +cp -f "$TA/src/api/middleware/rate-limit.ts" "$PTA/src/api/middleware/rate-limit.ts" +mkdir -p "$PTA/src/api/middleware" +cp -f "$TA/src/api/middleware/omnl-guards.ts" "$PTA/src/api/middleware/omnl-guards.ts" +cp -f "$TA/src/api/middleware/omnl-guards.test.ts" "$PTA/src/api/middleware/omnl-guards.test.ts" 2>/dev/null || true +cp -f "$TA/src/services/omnl-webhooks.test.ts" "$PTA/src/services/omnl-webhooks.test.ts" 2>/dev/null || true +mkdir -p "$PTA/src/resources" +cp -f "$TA/src/resources/omnl-openapi.json" "$PTA/src/resources/omnl-openapi.json" +cp -f "$TA/public/omnl-dashboard.html" "$PTA/public/omnl-dashboard.html" +cp -f "$TA/package.json" "$PTA/package.json" +cp -f "$ROOT/docs/deployment/DEPLOYMENT_INDEX.md" "$PUBLISH_ROOT/docs/deployment/DEPLOYMENT_INDEX.md" 2>/dev/null || true +cp -f "$ROOT/docs/MASTER_DOCUMENTATION_INDEX.md" "$PUBLISH_ROOT/docs/MASTER_DOCUMENTATION_INDEX.md" 2>/dev/null || true +cp -f "$ROOT/README.md" "$PUBLISH_ROOT/README.md" 2>/dev/null || true + +# Publish verify-deployment stays lightweight (no Forge) +cat > "$PUBLISH_ROOT/scripts/hybx-omnl/verify-deployment.sh" <<'EOS' +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" +echo "== validate cross-chain config ==" +node scripts/hybx-omnl/validate-cross-chain-config.mjs +cd services/token-aggregation +echo "== token-aggregation tsc ==" +pnpm exec tsc --noEmit +echo "== omnl reconcile report ==" +node scripts/omnl-reconcile-report.mjs >/dev/null +echo "verify-deployment (publish): OK" +EOS +chmod +x "$PUBLISH_ROOT/scripts/hybx-omnl/verify-deployment.sh" + +echo "sync-to-publish: OK → $PUBLISH_ROOT" diff --git a/scripts/hybx-omnl/validate-cross-chain-config.mjs b/scripts/hybx-omnl/validate-cross-chain-config.mjs new file mode 100755 index 0000000..7e4338d --- /dev/null +++ b/scripts/hybx-omnl/validate-cross-chain-config.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +/** + * Validate hybx-omnl-cross-chain-lines.json: structure, lineId hex, token addresses. + * Exit 0 on success; exit 1 on validation errors (stderr). + */ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +function isAddr(s) { + return typeof s === 'string' && /^0x[a-fA-F0-9]{40}$/.test(s); +} + +const root = resolve(__dirname, '../..'); +const configPath = process.env.OMNL_CROSS_CHAIN_CONFIG || resolve(root, 'config/hybx-omnl-cross-chain-lines.json'); + +function fail(msg) { + console.error(msg); + process.exit(1); +} + +const raw = readFileSync(configPath, 'utf8'); +let data; +try { + data = JSON.parse(raw); +} catch (e) { + fail(`Invalid JSON: ${configPath}: ${e}`); +} + +if (!data.lines || !Array.isArray(data.lines)) { + fail('Expected top-level { lines: [...] }'); +} + +for (let i = 0; i < data.lines.length; i++) { + const row = data.lines[i]; + const lid = row.lineId; + if (typeof lid !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(lid)) { + fail(`lines[${i}].lineId must be bytes32 hex: ${lid}`); + } + if (!row.chains || typeof row.chains !== 'object') { + fail(`lines[${i}].chains must be an object`); + } + for (const [cid, pair] of Object.entries(row.chains)) { + if (!/^\d+$/.test(cid)) fail(`lines[${i}].chains key must be chain id string: ${cid}`); + if (!pair || typeof pair !== 'object') fail(`lines[${i}].chains[${cid}] invalid`); + const p = pair; + if (!isAddr(p.tokenM0)) fail(`lines[${i}].chains[${cid}].tokenM0 invalid address: ${p.tokenM0}`); + if (!isAddr(p.tokenM1)) fail(`lines[${i}].chains[${cid}].tokenM1 invalid address: ${p.tokenM1}`); + } +} + +console.log(`OK: ${configPath} (${data.lines.length} line(s))`); diff --git a/scripts/hybx-omnl/verify-deployment.sh b/scripts/hybx-omnl/verify-deployment.sh new file mode 100755 index 0000000..ad93067 --- /dev/null +++ b/scripts/hybx-omnl/verify-deployment.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" +echo "== validate cross-chain config ==" +node scripts/hybx-omnl/validate-cross-chain-config.mjs +echo "== forge test hybx-omnl ==" +bash scripts/forge/scope.sh test hybx-omnl +echo "== token-aggregation build ==" +cd services/token-aggregation +pnpm exec tsc --noEmit +echo "verify-deployment: OK" diff --git a/services/token-aggregation/public/omnl-dashboard.html b/services/token-aggregation/public/omnl-dashboard.html new file mode 100644 index 0000000..dbcdae5 --- /dev/null +++ b/services/token-aggregation/public/omnl-dashboard.html @@ -0,0 +1,47 @@ + + + + + + HYBX OMNL — status + + + +

OMNL API snapshot

+

Reads /api/v1/omnl/ipsas/registry and /api/v1/omnl/ipsas/fineract-compare (503 if Fineract env missing; 401 if OMNL_API_KEY is set and no access_token in this page URL).

+

OpenAPI 3 JSON · OMNL API catalog · integration status · registry JSON · matrix JSON · Fineract health

+

IPSAS registry

+
Loading…
+

Fineract compare

+
Loading…
+ + + diff --git a/services/token-aggregation/scripts/encode-omnl-mirror-payload.mjs b/services/token-aggregation/scripts/encode-omnl-mirror-payload.mjs new file mode 100644 index 0000000..9395488 --- /dev/null +++ b/services/token-aggregation/scripts/encode-omnl-mirror-payload.mjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node +/** + * Encode OMNLMirrorReceiver CCIP data field (v2 with merkleRoot). + * Usage: node encode-omnl-mirror-payload.mjs + */ +import { AbiCoder } from 'ethers'; + +const [, , v, lineId, R, validUntil, evHash, merkleRoot] = process.argv; +if (!merkleRoot) { + console.error( + 'Usage: node encode-omnl-mirror-payload.mjs ' + ); + process.exit(1); +} +const coder = AbiCoder.defaultAbiCoder(); +const data = coder.encode( + ['uint256', 'bytes32', 'uint256', 'uint256', 'bytes32', 'bytes32'], + [BigInt(v), lineId, BigInt(R), BigInt(validUntil), evHash, merkleRoot] +); +process.stdout.write(data + '\n'); diff --git a/services/token-aggregation/scripts/omnl-attestation-payload.mjs b/services/token-aggregation/scripts/omnl-attestation-payload.mjs new file mode 100644 index 0000000..fa0a097 --- /dev/null +++ b/services/token-aggregation/scripts/omnl-attestation-payload.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node +/** + * Preview digest for off-chain attestation signing (fiat / XAU custodian flow). + * Uses keccak256(abi.encode(lineId, R, nonce, validUntil, evidenceKind)) — align with ops before HSM. + * Usage: node omnl-attestation-payload.mjs + */ +import { keccak256, AbiCoder } from 'ethers'; + +const [, , lineId, R, nonce, validUntil, evidenceKind] = process.argv; +if (!evidenceKind) { + console.error( + 'Usage: omnl-attestation-payload.mjs ' + ); + process.exit(1); +} +const coder = AbiCoder.defaultAbiCoder(); +const encoded = coder.encode( + ['bytes32', 'uint256', 'uint256', 'uint256', 'string'], + [lineId, BigInt(R), BigInt(nonce), BigInt(validUntil), evidenceKind] +); +const digest = keccak256(encoded); +process.stdout.write(JSON.stringify({ digest, encoded }, null, 2) + '\n'); diff --git a/services/token-aggregation/scripts/omnl-reconcile-report.mjs b/services/token-aggregation/scripts/omnl-reconcile-report.mjs new file mode 100644 index 0000000..a2d350b --- /dev/null +++ b/services/token-aggregation/scripts/omnl-reconcile-report.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/** + * Canonical reconciliation hash: IPSAS registry + journal matrix JSON (minor units / narratives unchanged). + * Exit 0 always; stdout JSON with sha256 for audit trail anchoring. + * Env: OMNL_IPSAS_GL_REGISTRY, OMNL_JOURNAL_MATRIX_PATH (optional overrides). + */ +import { readFileSync } from 'fs'; +import { createHash } from 'crypto'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const regPath = + process.env.OMNL_IPSAS_GL_REGISTRY || + resolve(__dirname, '../../../config/omnl-ipsas-gl-registry.json'); +const matrixPath = + process.env.OMNL_JOURNAL_MATRIX_PATH || + resolve(__dirname, '../../../config/omnl-journal-matrix.json'); + +const reg = readFileSync(regPath, 'utf8'); +const matrix = readFileSync(matrixPath, 'utf8'); +const canonical = JSON.stringify({ + registry: JSON.parse(reg), + matrix: JSON.parse(matrix), +}); +const sha256 = createHash('sha256').update(canonical).digest('hex'); +const out = { + ok: true, + sha256, + registryPath: regPath, + matrixPath: matrixPath, + generatedAt: new Date().toISOString(), +}; +process.stdout.write(JSON.stringify(out, null, 2) + '\n'); diff --git a/services/token-aggregation/scripts/omnl-reconcile-stub.mjs b/services/token-aggregation/scripts/omnl-reconcile-stub.mjs new file mode 100644 index 0000000..2ad8e3c --- /dev/null +++ b/services/token-aggregation/scripts/omnl-reconcile-stub.mjs @@ -0,0 +1,13 @@ +#!/usr/bin/env node +/** + * Back-compat entry: delegates to IPSAS + journal-matrix anchor (`omnl-reconcile-report.mjs`). + * For custodian/bank ↔ on-chain Merkle workflows, extend that script or add a separate job; do not fork the anchor format without updating ops runbooks. + */ +import { spawnSync } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const dir = dirname(fileURLToPath(import.meta.url)); +const report = join(dir, 'omnl-reconcile-report.mjs'); +const r = spawnSync(process.execPath, [report], { stdio: 'inherit' }); +process.exit(r.status ?? 1); diff --git a/services/token-aggregation/scripts/omnl-ttl-monitor.mjs b/services/token-aggregation/scripts/omnl-ttl-monitor.mjs new file mode 100644 index 0000000..cfef3eb --- /dev/null +++ b/services/token-aggregation/scripts/omnl-ttl-monitor.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +/** + * Exit 1 if reporting is not compliant (policy + freshness + ops) or RPC error. + * Env: RPC, OMNL_COMPLIANCE_CORE_*, OMNL_MONITOR_LINE_ID, OMNL_MONITOR_CHAIN_ID + */ +import { Contract, JsonRpcProvider } from 'ethers'; + +const ABI = [ + 'function getCompliance(bytes32 lineId) view returns (uint256 s0, uint256 s1, uint256 r, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, uint256 minR, uint256 maxS1, bool m0Ok, bool m1Ok, bool attestationStale, bool policyOk, bool operational, bool reportingCompliant)', +]; + +async function main() { + const chainId = parseInt(process.env.OMNL_MONITOR_CHAIN_ID || '138', 10); + const lineId = process.env.OMNL_MONITOR_LINE_ID; + const addr = + chainId === 138 + ? process.env.OMNL_COMPLIANCE_CORE_138 + : chainId === 651940 + ? process.env.OMNL_COMPLIANCE_CORE_651940 + : process.env[`OMNL_COMPLIANCE_CORE_${chainId}`]; + const rpc = + chainId === 138 + ? process.env.RPC_URL_138 || process.env.RPC_URL + : process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global'; + + if (!lineId || !addr || !rpc) { + console.error('Set OMNL_MONITOR_LINE_ID, OMNL_COMPLIANCE_CORE_*, and RPC for chain'); + process.exit(2); + } + + const provider = new JsonRpcProvider(rpc, chainId); + const c = new Contract(addr, ABI, provider); + const r = await c.getCompliance(lineId); + const ok = r.reportingCompliant; + console.log( + JSON.stringify({ + chainId, + reportingCompliant: ok, + policyOk: r.policyOk, + attestationStale: r.attestationStale, + operational: r.operational, + validUntil: r.validUntil.toString(), + }) + ); + if (!ok) process.exit(1); +} + +main().catch((e) => { + console.error(e); + process.exit(2); +}); diff --git a/services/token-aggregation/src/api/middleware/omnl-guards.test.ts b/services/token-aggregation/src/api/middleware/omnl-guards.test.ts new file mode 100644 index 0000000..ec01e41 --- /dev/null +++ b/services/token-aggregation/src/api/middleware/omnl-guards.test.ts @@ -0,0 +1,37 @@ +import type { Request, Response } from 'express'; +import { omnlSensitiveRouteGuard } from './omnl-guards'; + +describe('omnlSensitiveRouteGuard', () => { + const oldKey = process.env.OMNL_API_KEY; + + afterEach(() => { + if (oldKey === undefined) delete process.env.OMNL_API_KEY; + else process.env.OMNL_API_KEY = oldKey; + }); + + it('passes when OMNL_API_KEY unset', () => { + delete process.env.OMNL_API_KEY; + const next = jest.fn(); + omnlSensitiveRouteGuard({ headers: {}, query: {} } as unknown as Request, {} as Response, next); + expect(next).toHaveBeenCalled(); + }); + + it('401 when key set and no auth', () => { + process.env.OMNL_API_KEY = 'abc'; + const json = jest.fn(); + const status = jest.fn().mockReturnValue({ json }); + omnlSensitiveRouteGuard({ headers: {}, query: {} } as unknown as Request, { status } as unknown as Response, jest.fn()); + expect(status).toHaveBeenCalledWith(401); + }); + + it('passes with Bearer token', () => { + process.env.OMNL_API_KEY = 'abc'; + const next = jest.fn(); + omnlSensitiveRouteGuard( + { headers: { authorization: 'Bearer abc' }, query: {} } as unknown as Request, + {} as Response, + next + ); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/services/token-aggregation/src/api/middleware/omnl-guards.ts b/services/token-aggregation/src/api/middleware/omnl-guards.ts new file mode 100644 index 0000000..51437be --- /dev/null +++ b/services/token-aggregation/src/api/middleware/omnl-guards.ts @@ -0,0 +1,21 @@ +import type { Request, Response, NextFunction } from 'express'; + +/** + * When `OMNL_API_KEY` is set, require `Authorization: Bearer ` or `?access_token=`. + * If unset, all requests pass (backwards compatible). + */ +export function omnlSensitiveRouteGuard(req: Request, res: Response, next: NextFunction): void { + const key = process.env.OMNL_API_KEY?.trim(); + if (!key) { + next(); + return; + } + const auth = String(req.headers.authorization || ''); + const bearer = auth.startsWith('Bearer ') ? auth.slice(7).trim() : ''; + const q = String(req.query.access_token ?? '').trim(); + if (bearer === key || q === key) { + next(); + return; + } + res.status(401).json({ error: 'Unauthorized', hint: 'Set Authorization: Bearer or access_token for OMNL_API_KEY' }); +} diff --git a/services/token-aggregation/src/api/routes/omnl-ipsas.ts b/services/token-aggregation/src/api/routes/omnl-ipsas.ts new file mode 100644 index 0000000..1aba7d4 --- /dev/null +++ b/services/token-aggregation/src/api/routes/omnl-ipsas.ts @@ -0,0 +1,222 @@ +import { Router, Request, Response } from 'express'; +import { omnlRateLimiter } from '../middleware/rate-limit'; +import { omnlSensitiveRouteGuard } from '../middleware/omnl-guards'; +import { + loadIpsasRegistry, + validateJournalPairWithMatrix, + fetchFineractGlAccounts, + compareRegistryToFineract, + checkFineractConnectivity, +} from '../../services/omnl-ipsas-gl'; +import { loadJournalMatrix } from '../../services/omnl-journal-matrix'; +import { fetchOmnlCompliance, fetchOmnlComplianceAggregated } from '../../services/omnl-compliance'; + +const router = Router(); +router.use(omnlRateLimiter); + +/** + * GET /omnl/ipsas/registry — full IPSAS GL registry (codes, pairs, monetary layer hints). + */ +router.get('/omnl/ipsas/registry', (_req: Request, res: Response) => { + try { + res.json(loadIpsasRegistry()); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/ipsas/matrix — journal matrix (T-001…T-008) for Fineract posting alignment. + */ +router.get('/omnl/ipsas/matrix', (_req: Request, res: Response) => { + try { + res.json(loadJournalMatrix()); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/ipsas/validate-pair?debitGlCode=&creditGlCode= + * Ensures Debit/Credit GL codes appear in IPSAS registry or approved journal matrix. + */ +/** + * POST /omnl/ipsas/validate-pairs — body `{ "pairs": [{ "debitGlCode", "creditGlCode" }, ...] }` + */ +router.post('/omnl/ipsas/validate-pairs', (req: Request, res: Response) => { + try { + const raw = (req.body as { pairs?: unknown })?.pairs; + if (!Array.isArray(raw) || raw.length === 0) { + res.status(400).json({ error: 'JSON body must include non-empty pairs: [{ debitGlCode, creditGlCode }, ...]' }); + return; + } + const registry = loadIpsasRegistry(); + let matrix = null; + try { + matrix = loadJournalMatrix(); + } catch { + matrix = null; + } + const results = raw.map((p: unknown) => { + const row = p as { debitGlCode?: string; creditGlCode?: string }; + const debitGlCode = String(row?.debitGlCode ?? '').trim(); + const creditGlCode = String(row?.creditGlCode ?? '').trim(); + if (!debitGlCode || !creditGlCode) { + return { + debitGlCode, + creditGlCode, + error: 'debitGlCode and creditGlCode required per pair', + }; + } + const v = validateJournalPairWithMatrix(registry, matrix, debitGlCode, creditGlCode); + return { + debitGlCode, + creditGlCode, + ipsasCompliantPair: v.valid, + ipsasRef: v.ipsasRef, + memo: v.memo, + narrative: v.narrative, + source: v.source, + }; + }); + res.json({ generatedAt: new Date().toISOString(), count: results.length, results }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +router.get('/omnl/ipsas/validate-pair', (req: Request, res: Response) => { + try { + const debitGlCode = String(req.query.debitGlCode ?? '').trim(); + const creditGlCode = String(req.query.creditGlCode ?? '').trim(); + if (!debitGlCode || !creditGlCode) { + res.status(400).json({ error: 'debitGlCode and creditGlCode query params required' }); + return; + } + const registry = loadIpsasRegistry(); + let matrix = null; + try { + matrix = loadJournalMatrix(); + } catch { + matrix = null; + } + const v = validateJournalPairWithMatrix(registry, matrix, debitGlCode, creditGlCode); + const debitKnown = registry.accounts.some((a) => a.glCode === debitGlCode); + const creditKnown = registry.accounts.some((a) => a.glCode === creditGlCode); + res.json({ + debitGlCode, + creditGlCode, + ipsasCompliantPair: v.valid, + ipsasRef: v.ipsasRef, + memo: v.memo, + narrative: v.narrative, + source: v.source, + glCodesKnownInRegistry: { debit: debitKnown, credit: creditKnown }, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/ipsas/fineract-health — probe Fineract `/glaccounts?limit=1` (credentials from OMNL_FINERACT_*). + */ +router.get('/omnl/ipsas/fineract-health', async (_req: Request, res: Response) => { + try { + const r = await checkFineractConnectivity(); + const configured = Boolean( + (process.env.OMNL_FINERACT_BASE_URL || '').trim() && (process.env.OMNL_FINERACT_PASSWORD || '').trim() + ); + res.json({ + generatedAt: new Date().toISOString(), + configured, + ...r, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/ipsas/fineract-compare — live Fineract /glaccounts vs IPSAS registry codes (requires OMNL_FINERACT_*). + */ +router.get('/omnl/ipsas/fineract-compare', omnlSensitiveRouteGuard, async (_req: Request, res: Response) => { + try { + const registry = loadIpsasRegistry(); + const rows = await fetchFineractGlAccounts(); + const cmp = compareRegistryToFineract(registry, rows); + res.json({ + ...cmp, + fineractAccountCount: rows.length, + ipsasCompliant: cmp.aligned, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(503).json({ error: msg }); + } +}); + +/** + * GET /omnl/ipsas/layer/:layer — monetary layer hint (m0_reserve | m1_liability | settlement | equity). + */ +router.get('/omnl/ipsas/layer/:layer', (req: Request, res: Response) => { + try { + const layer = req.params.layer; + const registry = loadIpsasRegistry(); + const hint = registry.monetaryLayerHints[layer]; + if (!hint) { + res.status(404).json({ + error: 'Unknown layer', + known: Object.keys(registry.monetaryLayerHints), + }); + return; + } + res.json({ layer, ...hint }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/ipsas/compliance-context/:lineId?aggregated=1 + * On-chain compliance snapshot + IPSAS GL hints for OMNL / Fineract alignment. + */ +router.get('/omnl/ipsas/compliance-context/:lineId', omnlSensitiveRouteGuard, async (req: Request, res: Response) => { + try { + const lineId = req.params.lineId as string; + const aggregated = + req.query.aggregated === '1' || String(req.query.aggregated || '').toLowerCase() === 'true'; + const registry = loadIpsasRegistry(); + let compliance: unknown; + if (aggregated) { + compliance = await fetchOmnlComplianceAggregated(lineId); + } else { + const addr = process.env.OMNL_COMPLIANCE_CORE_138; + if (!addr) { + res.status(503).json({ error: 'OMNL_COMPLIANCE_CORE_138 not set' }); + return; + } + compliance = await fetchOmnlCompliance(138, lineId, addr); + } + res.json({ + compliance, + ipsas: { + monetaryLayerHints: registry.monetaryLayerHints, + postingGuidance: + 'Reserve movements (M0) map to GL 1050; M1 liabilities to 2000/2100; settlement to 1000. Validate every journal with GET /api/v1/omnl/ipsas/validate-pair before POST /journalentries.', + fineractDocs: 'https://omnl.hybxfinance.io/fineract-provider/swagger-ui/index.html', + }, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +export default router; diff --git a/services/token-aggregation/src/api/routes/omnl.ts b/services/token-aggregation/src/api/routes/omnl.ts new file mode 100644 index 0000000..a317708 --- /dev/null +++ b/services/token-aggregation/src/api/routes/omnl.ts @@ -0,0 +1,324 @@ +import { Router, Request, Response } from 'express'; +import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers'; +import { omnlRateLimiter } from '../middleware/rate-limit'; +import { + fetchOmnlCompliance, + fetchOmnlComplianceAggregated, + fetchLatestAttestation, + fetchBreakerStatus, + loadCrossChainLines, + loadCrossChainConfigPath, +} from '../../services/omnl-compliance'; +import { computeOmnlReconcileAnchor } from '../../services/omnl-reconcile-anchor'; +import { getOmnlIntegrationStatus } from '../../services/omnl-integration-status'; +import { getOmnlApiCatalog } from '../../services/omnl-api-catalog'; +import omnlOpenApi from '../../resources/omnl-openapi.json'; + +const router = Router(); +router.use(omnlRateLimiter); + +const REGISTRY_ABI: InterfaceAbi = [ + 'function allLineIds() view returns (bytes32[])', + 'function getLine(bytes32 lineId) view returns (tuple(address tokenM0,address tokenM1,uint8 decimals,uint16 iso4217Numeric,bool isXAU,bool active))', +]; + +const COORDINATOR_ABI: InterfaceAbi = [ + 'function mirrorChainSelector() view returns (uint64)', + 'function mirrorReceiver() view returns (address)', + 'function feeToken() view returns (address)', +]; + +/** + * GET /omnl/openapi.json — OpenAPI 3.0 document for Swagger UI / codegen. + */ +router.get('/omnl/openapi.json', (_req: Request, res: Response) => { + res.type('application/json').json(omnlOpenApi as Record); +}); + +/** + * GET /omnl/catalog — machine-readable list of OMNL HTTP integrations. + */ +router.get('/omnl/catalog', (_req: Request, res: Response) => { + res.json(getOmnlApiCatalog()); +}); + +/** + * GET /omnl/integration-status — which env-backed integrations are present (no secrets). + */ +router.get('/omnl/integration-status', (_req: Request, res: Response) => { + res.json({ + generatedAt: new Date().toISOString(), + ...getOmnlIntegrationStatus(), + }); +}); + +/** + * GET /omnl/reconcile-anchor — same SHA-256 as omnl-reconcile-report.mjs (IPSAS + matrix files). + */ +router.get('/omnl/reconcile-anchor', (_req: Request, res: Response) => { + const out = computeOmnlReconcileAnchor(); + if (!out.ok) { + res.status(503).json(out); + return; + } + res.json(out); +}); + +/** + * GET /omnl/cross-chain-lines — logical lines from hybx-omnl-cross-chain-lines.json. + */ +router.get('/omnl/cross-chain-lines', (_req: Request, res: Response) => { + try { + res.json({ + configPath: loadCrossChainConfigPath(), + lines: loadCrossChainLines(), + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/mirror-coordinator?chainId=138 — on-chain mirror destination (CCIP). + */ +/** + * GET /omnl/zk-verifier — env-configured verifier address (placeholder ok). + */ +router.get('/omnl/zk-verifier', (_req: Request, res: Response) => { + const a = process.env.OMNL_ZK_VERIFIER?.trim() || ''; + res.json({ + configured: Boolean(a), + address: a || null, + note: 'See docs/hybx-omnl/ZK_INTEGRATION.md for wiring a real verifier.', + }); +}); + +router.get('/omnl/mirror-coordinator', async (req: Request, res: Response) => { + try { + const chainId = parseInt(String(req.query.chainId || '138'), 10); + const coord = + process.env[`OMNL_MIRROR_COORDINATOR_${chainId}`]?.trim() || + process.env.OMNL_MIRROR_COORDINATOR?.trim(); + if (!coord) { + res.status(503).json({ + error: 'OMNL_MIRROR_COORDINATOR or OMNL_MIRROR_COORDINATOR_ not set', + chainId, + }); + return; + } + const rpc = rpcUrl(chainId); + if (!rpc) { + res.status(503).json({ error: 'RPC not configured for chain', chainId }); + return; + } + const c = new Contract(coord, COORDINATOR_ABI, new JsonRpcProvider(rpc, chainId)); + const [mirrorChainSelector, mirrorReceiver, feeToken] = await Promise.all([ + c.mirrorChainSelector(), + c.mirrorReceiver(), + c.feeToken(), + ]); + res.json({ + chainId, + coordinator: coord, + mirrorChainSelector: String(mirrorChainSelector), + mirrorReceiver, + feeToken, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +function addrForChain(chainId: number): string | undefined { + if (chainId === 138) { + return process.env.OMNL_COMPLIANCE_CORE_138; + } + if (chainId === 651940) { + return process.env.OMNL_COMPLIANCE_CORE_651940; + } + return process.env[`OMNL_COMPLIANCE_CORE_${chainId}`]; +} + +function rpcUrl(chainId: number): string | undefined { + if (chainId === 138) return process.env.RPC_URL_138 || process.env.RPC_URL; + if (chainId === 651940) return process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global'; + return process.env[`CHAIN_${chainId}_RPC_URL`]; +} + +/** + * GET /omnl/compliance/:lineId?chainId=138|651940 + */ +router.get('/omnl/compliance/:lineId', async (req: Request, res: Response) => { + try { + const chainId = parseInt(String(req.query.chainId || '138'), 10); + const lineId = req.params.lineId as string; + const addr = addrForChain(chainId); + if (!addr) { + res.status(503).json({ + error: 'OMNL_COMPLIANCE_CORE not configured for this chain', + chainId, + }); + return; + } + const snap = await fetchOmnlCompliance(chainId, lineId, addr); + res.json(snap); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/compliance-aggregated/:lineId — summed supply across chains (see hybx-omnl-cross-chain-lines.json). + */ +router.get('/omnl/compliance-aggregated/:lineId', async (req: Request, res: Response) => { + try { + const lineId = req.params.lineId as string; + const snap = await fetchOmnlComplianceAggregated(lineId); + res.json(snap); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/instruments?chainId=138 — InstrumentRegistry.allLineIds + getLine + */ +router.get('/omnl/instruments', async (req: Request, res: Response) => { + try { + const chainId = parseInt(String(req.query.chainId || '138'), 10); + const reg = process.env[`OMNL_INSTRUMENT_REGISTRY_${chainId}`] || process.env.OMNL_INSTRUMENT_REGISTRY_138; + const rpc = rpcUrl(chainId); + if (!reg || !rpc) { + res.status(503).json({ error: 'OMNL_INSTRUMENT_REGISTRY_* and RPC required', chainId }); + return; + } + const c = new Contract(reg, REGISTRY_ABI, new JsonRpcProvider(rpc, chainId)); + const ids = (await c.allLineIds()) as string[]; + const lines = await Promise.all( + ids.map(async (id: string) => { + const line = await c.getLine(id); + return { lineId: id, line }; + }) + ); + res.json({ chainId, lines, crossChainConfigLines: loadCrossChainLines().length }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/attestations/:lineId?chainId=138 — ReserveCommitmentStore.getCommitment + */ +router.get('/omnl/attestations/:lineId', async (req: Request, res: Response) => { + try { + const chainId = parseInt(String(req.query.chainId || '138'), 10); + const lineId = req.params.lineId as string; + const out = await fetchLatestAttestation(chainId, lineId); + res.json({ chainId, lineId, ...out }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/breaker?chainId=138&lineId=0x... + */ +router.get('/omnl/breaker', async (req: Request, res: Response) => { + try { + const chainId = parseInt(String(req.query.chainId || '138'), 10); + const lineId = String(req.query.lineId || ''); + if (!lineId || lineId === 'undefined') { + res.status(400).json({ error: 'lineId query parameter required' }); + return; + } + const s = await fetchBreakerStatus(chainId, lineId); + res.json({ chainId, lineId, ...s }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/mirror-status/:lineId — compare reserve version (and R) across 138 and 651940. + */ +router.get('/omnl/mirror-status/:lineId', async (req: Request, res: Response) => { + try { + const lineId = req.params.lineId as string; + const a138 = process.env.OMNL_RESERVE_STORE_138; + const a651940 = process.env.OMNL_RESERVE_STORE_651940; + const out: Record = { + lineId, + configured138: Boolean(a138), + configured651940: Boolean(a651940), + }; + if (!a138 || !a651940) { + res.status(503).json({ + ...out, + error: 'Set OMNL_RESERVE_STORE_138 and OMNL_RESERVE_STORE_651940 for mirror comparison', + }); + return; + } + const [x138, x651] = await Promise.all([ + fetchLatestAttestation(138, lineId), + fetchLatestAttestation(651940, lineId), + ]); + out.attestation138 = x138; + out.attestation651940 = x651; + out.rSynced = x138.r === x651.r; + out.versionSynced = x138.version === x651.version; + out.merkleSynced = x138.merkleRoot === x651.merkleRoot; + out.inSync = out.rSynced && out.versionSynced; + res.json(out); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ error: msg }); + } +}); + +/** + * GET /omnl/health — requires OMNL_HEALTH_LINE_ID when comparing chains (no default zero line). + */ +router.get('/omnl/health', async (req: Request, res: Response) => { + const line = process.env.OMNL_HEALTH_LINE_ID; + if (!line) { + res.status(400).json({ + error: 'Set OMNL_HEALTH_LINE_ID to a registered bytes32 line id (0x-prefixed)', + }); + return; + } + const a138 = process.env.OMNL_COMPLIANCE_CORE_138; + const a651940 = process.env.OMNL_COMPLIANCE_CORE_651940; + const out: Record = { + lineId: line, + configured138: Boolean(a138), + configured651940: Boolean(a651940), + }; + try { + if (a138) { + out.compliance138 = await fetchOmnlCompliance(138, line, a138); + } + if (a651940) { + out.compliance651940 = await fetchOmnlCompliance(651940, line, a651940); + } + if (a138 && a651940 && out.compliance138 && out.compliance651940) { + const x = out.compliance138 as { r: string; reportingCompliant: boolean }; + const y = out.compliance651940 as { r: string; reportingCompliant: boolean }; + out.rSynced = x.r === y.r; + out.reportingSynced = + x.reportingCompliant === y.reportingCompliant && x.r === y.r; + } + res.json(out); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.status(500).json({ ...out, error: msg }); + } +}); + +export default router; diff --git a/services/token-aggregation/src/indexer/omnl-event-poller.ts b/services/token-aggregation/src/indexer/omnl-event-poller.ts new file mode 100644 index 0000000..4f389d9 --- /dev/null +++ b/services/token-aggregation/src/indexer/omnl-event-poller.ts @@ -0,0 +1,86 @@ +import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers'; +import { logger } from '../utils/logger'; +import { emitOmnlWebhook } from '../services/omnl-webhooks'; +import { loadLastProcessedBlock, saveLastProcessedBlock } from './omnl-poller-state'; + +const RESERVE_ABI: InterfaceAbi = [ + 'event ReserveCommitted(bytes32 indexed lineId,uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 version,address indexed by)', +]; + +/** + * Polls ReserveCommitted on Chain 138 / 651940 for ops visibility. + * Persists last processed block per chain (`OMNL_POLLER_STATE_PATH` or `.omnl-poller-state.json`). + */ +export class OmnlEventPoller { + private timers: NodeJS.Timeout[] = []; + + start(): void { + const interval = parseInt(process.env.OMNL_EVENT_POLL_INTERVAL_MS || '60000', 10); + const chains = (process.env.OMNL_EVENT_POLL_CHAINS || '138,651940').split(',').map((s) => parseInt(s.trim(), 10)); + + for (const chainId of chains) { + const addr = + chainId === 138 + ? process.env.OMNL_RESERVE_STORE_138 + : chainId === 651940 + ? process.env.OMNL_RESERVE_STORE_651940 + : process.env[`OMNL_RESERVE_STORE_${chainId}`]; + const rpc = + chainId === 138 + ? process.env.RPC_URL_138 || process.env.RPC_URL + : process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global'; + if (!addr || !rpc) { + logger.warn(`OMNL event poll skipped chain ${chainId}: missing store or RPC`); + continue; + } + + const persisted = loadLastProcessedBlock(chainId); + let lastBlock = persisted ?? 0; + + const provider = new JsonRpcProvider(rpc, chainId); + const c = new Contract(addr, RESERVE_ABI, provider); + + const tick = async () => { + try { + const head = await provider.getBlockNumber(); + const from = lastBlock === 0 ? Math.max(0, head - 99) : lastBlock + 1; + if (from > head) return; + const evs = await c.queryFilter('ReserveCommitted', from, head); + for (const ev of evs) { + const deliveryId = `${chainId}-${ev.blockNumber}-${ev.index}`; + logger.info('OMNL ReserveCommitted', { chainId, logIndex: ev.index, deliveryId }); + const args = (ev as { args?: { lineId?: string; version?: bigint; R?: bigint } }).args; + void emitOmnlWebhook({ + event: 'ReserveCommitted', + chainId, + lineId: args?.lineId, + deliveryId, + payload: { + blockNumber: ev.blockNumber, + transactionHash: ev.transactionHash, + version: args?.version?.toString(), + r: args?.R?.toString(), + }, + ts: new Date().toISOString(), + }); + } + lastBlock = head; + saveLastProcessedBlock(chainId, head); + } catch (e) { + logger.error(`OMNL poll error chain ${chainId}`, e); + } + }; + + void tick(); + const t = setInterval(() => void tick(), interval); + this.timers.push(t); + } + } + + stop(): void { + for (const t of this.timers) { + clearInterval(t); + } + this.timers = []; + } +} diff --git a/services/token-aggregation/src/indexer/omnl-poller-state.ts b/services/token-aggregation/src/indexer/omnl-poller-state.ts new file mode 100644 index 0000000..cfedfb2 --- /dev/null +++ b/services/token-aggregation/src/indexer/omnl-poller-state.ts @@ -0,0 +1,41 @@ +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; + +export type OmnlPollerStateFile = { + chains: Record; +}; + +function statePath(): string { + return process.env.OMNL_POLLER_STATE_PATH?.trim() || join(process.cwd(), '.omnl-poller-state.json'); +} + +export function loadLastProcessedBlock(chainId: number): number | null { + const p = statePath(); + if (!existsSync(p)) return null; + try { + const j = JSON.parse(readFileSync(p, 'utf8')) as OmnlPollerStateFile; + const v = j.chains?.[String(chainId)]; + return typeof v === 'number' && v >= 0 ? v : null; + } catch { + return null; + } +} + +export function saveLastProcessedBlock(chainId: number, block: number): void { + const p = statePath(); + let j: OmnlPollerStateFile = { chains: {} }; + if (existsSync(p)) { + try { + const parsed = JSON.parse(readFileSync(p, 'utf8')) as OmnlPollerStateFile; + j = { chains: { ...parsed.chains } }; + } catch { + j = { chains: {} }; + } + } + j.chains[String(chainId)] = block; + const dir = dirname(p); + if (dir && dir !== '.') { + mkdirSync(dir, { recursive: true }); + } + writeFileSync(p, JSON.stringify(j, null, 2) + '\n', 'utf8'); +} diff --git a/services/token-aggregation/src/resources/omnl-openapi.json b/services/token-aggregation/src/resources/omnl-openapi.json new file mode 100644 index 0000000..31983d3 --- /dev/null +++ b/services/token-aggregation/src/resources/omnl-openapi.json @@ -0,0 +1,276 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "HYBX OMNL API", + "description": "HYBX OMNL endpoints on the token-aggregation service (Chain 138 / ALL Mainnet 651940, IPSAS, Fineract alignment). Optional auth: when `OMNL_API_KEY` is set, use `Authorization: Bearer ` or query `access_token`.", + "version": "1.2.0" + }, + "servers": [{ "url": "/api/v1", "description": "Token aggregation v1" }], + "tags": [ + { "name": "omnl-discovery", "description": "Catalog and probes" }, + { "name": "omnl-chain", "description": "On-chain compliance and instruments" }, + { "name": "omnl-ipsas", "description": "IPSAS GL and Fineract" } + ], + "paths": { + "/omnl/openapi.json": { + "get": { + "tags": ["omnl-discovery"], + "summary": "OpenAPI 3 document (this file)", + "operationId": "getOmnlOpenApi", + "responses": { "200": { "description": "OpenAPI 3 JSON" } } + } + }, + "/omnl/catalog": { + "get": { + "tags": ["omnl-discovery"], + "summary": "Human/agent catalog of OMNL routes", + "operationId": "getOmnlCatalog", + "responses": { "200": { "description": "Catalog object" } } + } + }, + "/omnl/integration-status": { + "get": { + "tags": ["omnl-discovery"], + "summary": "Which env-backed integrations are configured", + "operationId": "getOmnlIntegrationStatus", + "responses": { "200": { "description": "Status flags" } } + } + }, + "/omnl/reconcile-anchor": { + "get": { + "tags": ["omnl-discovery"], + "summary": "SHA-256 anchor for IPSAS registry + journal matrix files", + "operationId": "getOmnlReconcileAnchor", + "responses": { "200": { "description": "Anchor payload" }, "503": { "description": "Config files missing" } } + } + }, + "/omnl/cross-chain-lines": { + "get": { + "tags": ["omnl-discovery"], + "summary": "hybx-omnl-cross-chain-lines.json", + "operationId": "getOmnlCrossChainLines", + "responses": { "200": { "description": "lines + configPath" } } + } + }, + "/omnl/zk-verifier": { + "get": { + "tags": ["omnl-discovery"], + "summary": "ZK verifier address from env", + "operationId": "getOmnlZkVerifier", + "responses": { "200": { "description": "address or null" } } + } + }, + "/omnl/mirror-coordinator": { + "get": { + "tags": ["omnl-chain"], + "summary": "Read OMNLMirrorCoordinator mirror destination", + "operationId": "getOmnlMirrorCoordinator", + "parameters": [ + { + "name": "chainId", + "in": "query", + "schema": { "type": "integer", "default": 138 }, + "description": "Chain where coordinator is deployed" + } + ], + "responses": { "200": { "description": "selector, receiver, feeToken" }, "503": { "description": "Not configured" } } + } + }, + "/omnl/compliance/{lineId}": { + "get": { + "tags": ["omnl-chain"], + "summary": "ComplianceCore snapshot", + "operationId": "getOmnlCompliance", + "parameters": [ + { "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }, + { + "name": "chainId", + "in": "query", + "schema": { "type": "integer", "default": 138 } + } + ], + "responses": { "200": { "description": "Compliance snapshot" } } + } + }, + "/omnl/compliance-aggregated/{lineId}": { + "get": { + "tags": ["omnl-chain"], + "summary": "Aggregated cross-chain compliance", + "operationId": "getOmnlComplianceAggregated", + "parameters": [{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }], + "responses": { "200": { "description": "Aggregated snapshot" } } + } + }, + "/omnl/instruments": { + "get": { + "tags": ["omnl-chain"], + "summary": "InstrumentRegistry lines", + "operationId": "getOmnlInstruments", + "parameters": [ + { "name": "chainId", "in": "query", "schema": { "type": "integer", "default": 138 } } + ], + "responses": { "200": { "description": "lines" } } + } + }, + "/omnl/attestations/{lineId}": { + "get": { + "tags": ["omnl-chain"], + "summary": "Reserve commitment", + "operationId": "getOmnlAttestations", + "parameters": [ + { "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }, + { "name": "chainId", "in": "query", "schema": { "type": "integer", "default": 138 } } + ], + "responses": { "200": { "description": "Commitment tuple" } } + } + }, + "/omnl/breaker": { + "get": { + "tags": ["omnl-chain"], + "summary": "Circuit breaker status", + "operationId": "getOmnlBreaker", + "parameters": [ + { "name": "chainId", "in": "query", "schema": { "type": "integer", "default": 138 } }, + { "name": "lineId", "in": "query", "required": true, "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "Breaker state" } } + } + }, + "/omnl/mirror-status/{lineId}": { + "get": { + "tags": ["omnl-chain"], + "summary": "Compare reserve attestation across chains", + "operationId": "getOmnlMirrorStatus", + "parameters": [{ "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }], + "responses": { "200": { "description": "Comparison" } } + } + }, + "/omnl/health": { + "get": { + "tags": ["omnl-chain"], + "summary": "Multi-chain compliance health (requires OMNL_HEALTH_LINE_ID)", + "operationId": "getOmnlHealth", + "responses": { "200": { "description": "Health payload" }, "400": { "description": "Missing line id env" } } + } + }, + "/omnl/ipsas/registry": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "IPSAS GL registry JSON", + "operationId": "getOmnlIpsasRegistry", + "responses": { "200": { "description": "Registry" } } + } + }, + "/omnl/ipsas/matrix": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "Journal matrix JSON", + "operationId": "getOmnlIpsasMatrix", + "responses": { "200": { "description": "Matrix" } } + } + }, + "/omnl/ipsas/validate-pair": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "Validate one GL pair", + "operationId": "getOmnlIpsasValidatePair", + "parameters": [ + { "name": "debitGlCode", "in": "query", "required": true, "schema": { "type": "string" } }, + { "name": "creditGlCode", "in": "query", "required": true, "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "Validation result" } } + } + }, + "/omnl/ipsas/validate-pairs": { + "post": { + "tags": ["omnl-ipsas"], + "summary": "Batch validate GL pairs", + "operationId": "postOmnlIpsasValidatePairs", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["pairs"], + "properties": { + "pairs": { + "type": "array", + "items": { + "type": "object", + "required": ["debitGlCode", "creditGlCode"], + "properties": { + "debitGlCode": { "type": "string" }, + "creditGlCode": { "type": "string" } + } + } + } + } + } + } + } + }, + "responses": { "200": { "description": "Batch results" }, "400": { "description": "Invalid body" } } + } + }, + "/omnl/ipsas/fineract-health": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "Fineract API reachability probe", + "operationId": "getOmnlIpsasFineractHealth", + "responses": { "200": { "description": "Health payload" } } + } + }, + "/omnl/ipsas/fineract-compare": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "Registry vs live Fineract GL accounts", + "operationId": "getOmnlIpsasFineractCompare", + "security": [{ "OmnlApiKey": [] }, { "AccessTokenQuery": [] }], + "responses": { "200": { "description": "Compare result" }, "401": { "description": "When OMNL_API_KEY set" } } + } + }, + "/omnl/ipsas/layer/{layer}": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "Monetary layer hint", + "operationId": "getOmnlIpsasLayer", + "parameters": [{ "name": "layer", "in": "path", "required": true, "schema": { "type": "string" } }], + "responses": { "200": { "description": "Layer hint" }, "404": { "description": "Unknown layer" } } + } + }, + "/omnl/ipsas/compliance-context/{lineId}": { + "get": { + "tags": ["omnl-ipsas"], + "summary": "Compliance + IPSAS posting guidance", + "operationId": "getOmnlIpsasComplianceContext", + "parameters": [ + { "name": "lineId", "in": "path", "required": true, "schema": { "type": "string" } }, + { + "name": "aggregated", + "in": "query", + "schema": { "type": "string", "enum": ["1", "true"] }, + "description": "Set to 1 for aggregated view" + } + ], + "security": [{ "OmnlApiKey": [] }, { "AccessTokenQuery": [] }], + "responses": { "200": { "description": "Context" }, "401": { "description": "When OMNL_API_KEY set" } } + } + } + }, + "components": { + "securitySchemes": { + "OmnlApiKey": { + "type": "http", + "scheme": "bearer", + "description": "When OMNL_API_KEY is set: value equals that secret (Bearer)." + }, + "AccessTokenQuery": { + "type": "apiKey", + "in": "query", + "name": "access_token", + "description": "Alternate to Bearer when OMNL_API_KEY is set." + } + } + } +} diff --git a/services/token-aggregation/src/services/omnl-api-catalog.ts b/services/token-aggregation/src/services/omnl-api-catalog.ts new file mode 100644 index 0000000..da48d39 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-api-catalog.ts @@ -0,0 +1,50 @@ +/** Static catalog for integration tools (OpenAPI-style discovery without Swagger). */ +export function getOmnlApiCatalog(): { + service: string; + basePath: string; + version: string; + endpoints: Array<{ + method: string; + path: string; + description: string; + query?: string[]; + auth?: string; + body?: string; + }>; +} { + return { + service: 'HYBX OMNL', + basePath: '/api/v1', + version: '1.2.0', + endpoints: [ + { method: 'GET', path: '/omnl/openapi.json', description: 'OpenAPI 3.0 JSON (Swagger-compatible)', auth: 'none' }, + { method: 'GET', path: '/omnl/catalog', description: 'This catalog (machine-readable)', auth: 'none' }, + { method: 'GET', path: '/omnl/integration-status', description: 'Which env-backed integrations are configured', auth: 'none' }, + { method: 'GET', path: '/omnl/reconcile-anchor', description: 'SHA-256 of canonical IPSAS registry + journal matrix JSON', auth: 'none' }, + { method: 'GET', path: '/omnl/cross-chain-lines', description: 'hybx-omnl-cross-chain-lines.json lines[]', auth: 'none' }, + { method: 'GET', path: '/omnl/zk-verifier', description: 'ZK verifier address from OMNL_ZK_VERIFIER', auth: 'none' }, + { method: 'GET', path: '/omnl/mirror-coordinator', description: 'Read on-chain mirror destination', query: ['chainId'], auth: 'none' }, + { method: 'GET', path: '/omnl/compliance/:lineId', description: 'ComplianceCore snapshot', query: ['chainId'], auth: 'none' }, + { method: 'GET', path: '/omnl/compliance-aggregated/:lineId', description: 'Aggregated supply + policy', auth: 'none' }, + { method: 'GET', path: '/omnl/instruments', description: 'InstrumentRegistry lines', query: ['chainId'], auth: 'none' }, + { method: 'GET', path: '/omnl/attestations/:lineId', description: 'ReserveCommitmentStore commitment', query: ['chainId'], auth: 'none' }, + { method: 'GET', path: '/omnl/breaker', description: 'Circuit breaker status', query: ['chainId', 'lineId'], auth: 'none' }, + { method: 'GET', path: '/omnl/mirror-status/:lineId', description: 'Compare reserve attestation 138 vs 651940', auth: 'none' }, + { method: 'GET', path: '/omnl/health', description: 'Multi-chain compliance probe', query: [], auth: 'none' }, + { method: 'GET', path: '/omnl/ipsas/registry', description: 'IPSAS GL registry JSON', auth: 'none' }, + { method: 'GET', path: '/omnl/ipsas/matrix', description: 'Journal matrix JSON', auth: 'none' }, + { method: 'GET', path: '/omnl/ipsas/validate-pair', description: 'Validate one debit/credit pair', query: ['debitGlCode', 'creditGlCode'], auth: 'none' }, + { + method: 'POST', + path: '/omnl/ipsas/validate-pairs', + description: 'Batch validate journal pairs', + body: '{ pairs: [{ debitGlCode, creditGlCode }] }', + auth: 'none', + }, + { method: 'GET', path: '/omnl/ipsas/fineract-health', description: 'Fineract API reachability (glaccounts probe)', auth: 'none' }, + { method: 'GET', path: '/omnl/ipsas/fineract-compare', description: 'Registry vs live GL accounts', auth: 'OMNL_API_KEY if set' }, + { method: 'GET', path: '/omnl/ipsas/layer/:layer', description: 'Monetary layer hint', auth: 'none' }, + { method: 'GET', path: '/omnl/ipsas/compliance-context/:lineId', description: 'Compliance + IPSAS guidance', query: ['aggregated'], auth: 'OMNL_API_KEY if set' }, + ], + }; +} diff --git a/services/token-aggregation/src/services/omnl-compliance.ts b/services/token-aggregation/src/services/omnl-compliance.ts new file mode 100644 index 0000000..26583c9 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-compliance.ts @@ -0,0 +1,274 @@ +import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers'; +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; +import { isCompliant, maxM1ForM0, minReservesForM0 } from './omnl-policy-math'; + +const COMPLIANCE_ABI: InterfaceAbi = [ + 'function getCompliance(bytes32 lineId) view returns (uint256 s0, uint256 s1, uint256 r, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, uint256 minR, uint256 maxS1, bool m0Ok, bool m1Ok, bool attestationStale, bool policyOk, bool operational, bool reportingCompliant)', +]; + +const BREAKER_ABI: InterfaceAbi = ['function isLineOperational(bytes32 lineId) view returns (bool)']; + +const ERC20_ABI: InterfaceAbi = ['function totalSupply() view returns (uint256)']; + +const RESERVE_ABI: InterfaceAbi = [ + 'function getCommitment(bytes32 lineId) view returns (tuple(uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 version))', +]; + +export interface OmnlComplianceSnapshot { + chainId: number; + lineId: string; + s0: string; + s1: string; + r: string; + validUntil: string; + evidenceHash: string; + merkleRoot: string; + minR: string; + maxS1: string; + m0Ok: boolean; + m1Ok: boolean; + attestationStale: boolean; + policyOk: boolean; + operational: boolean; + reportingCompliant: boolean; +} + +export interface OmnlCrossChainLineEntry { + lineId: string; + chains: Record; +} + +export interface OmnlAggregatedSnapshot extends OmnlComplianceSnapshot { + aggregated: true; + s0ByChain: Record; + s1ByChain: Record; +} + +function rpcForChain(chainId: number): string | undefined { + if (chainId === 138) { + return process.env.RPC_URL_138 || process.env.RPC_URL; + } + if (chainId === 651940) { + return process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global'; + } + return process.env[`CHAIN_${chainId}_RPC_URL`]; +} + +function parseTuple(res: unknown): OmnlComplianceSnapshot & { chainId: number; lineId: string } { + const t = res as unknown as { + s0: bigint; + s1: bigint; + r: bigint; + validUntil: bigint; + evidenceHash: string; + merkleRoot: string; + minR: bigint; + maxS1: bigint; + m0Ok: boolean; + m1Ok: boolean; + attestationStale: boolean; + policyOk: boolean; + operational: boolean; + reportingCompliant: boolean; + }; + return { + chainId: 0, + lineId: '', + s0: t.s0.toString(), + s1: t.s1.toString(), + r: t.r.toString(), + validUntil: t.validUntil.toString(), + evidenceHash: t.evidenceHash, + merkleRoot: t.merkleRoot, + minR: t.minR.toString(), + maxS1: t.maxS1.toString(), + m0Ok: t.m0Ok, + m1Ok: t.m1Ok, + attestationStale: t.attestationStale, + policyOk: t.policyOk, + operational: t.operational, + reportingCompliant: t.reportingCompliant, + }; +} + +export async function fetchOmnlCompliance( + chainId: number, + lineIdHex: string, + complianceAddress: string +): Promise { + const url = rpcForChain(chainId); + if (!url) { + throw new Error(`No RPC configured for chain ${chainId}`); + } + const provider = new JsonRpcProvider(url, chainId); + const c = new Contract(complianceAddress, COMPLIANCE_ABI, provider); + const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`; + const res = await c.getCompliance(lineId); + const out = parseTuple(res); + out.chainId = chainId; + out.lineId = lineId; + return out; +} + +export function loadCrossChainConfigPath(): string { + return ( + process.env.OMNL_CROSS_CHAIN_CONFIG || + resolve(__dirname, '../../../../config/hybx-omnl-cross-chain-lines.json') + ); +} + +export function loadCrossChainLines(): OmnlCrossChainLineEntry[] { + const p = loadCrossChainConfigPath(); + if (!existsSync(p)) { + return []; + } + const raw = JSON.parse(readFileSync(p, 'utf8')) as { lines?: OmnlCrossChainLineEntry[] }; + return raw.lines ?? []; +} + +async function readTotalSupply(rpcUrl: string, chainId: number, token: string): Promise { + const provider = new JsonRpcProvider(rpcUrl, chainId); + const t = new Contract(token, ERC20_ABI, provider); + return (await t.totalSupply()) as bigint; +} + +/** + * Sum M0/M1 supply across chains for a logical line; use R/TTL from primary (138) reserve or compliance. + */ +export async function fetchOmnlComplianceAggregated(lineIdHex: string): Promise { + const addr138 = process.env.OMNL_COMPLIANCE_CORE_138; + if (!addr138) { + throw new Error('OMNL_COMPLIANCE_CORE_138 required for aggregated view'); + } + + const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`; + const lines = loadCrossChainLines(); + const entry = lines.find((l) => l.lineId.toLowerCase() === lineId.toLowerCase()); + if (!entry) { + throw new Error(`lineId not found in cross-chain config: ${lineId}`); + } + + const primary = await fetchOmnlCompliance(138, lineId, addr138); + const rpc138 = rpcForChain(138)!; + const rpc651 = rpcForChain(651940)!; + + let s0Sum = 0n; + let s1Sum = 0n; + const s0ByChain: Record = {}; + const s1ByChain: Record = {}; + + for (const [cid, pair] of Object.entries(entry.chains)) { + const chainNum = parseInt(cid, 10); + const rpc = chainNum === 138 ? rpc138 : chainNum === 651940 ? rpc651 : rpcForChain(chainNum); + if (!rpc) continue; + const a0 = await readTotalSupply(rpc, chainNum, pair.tokenM0); + const a1 = await readTotalSupply(rpc, chainNum, pair.tokenM1); + s0Sum += a0; + s1Sum += a1; + s0ByChain[cid] = a0.toString(); + s1ByChain[cid] = a1.toString(); + } + + const r = BigInt(primary.r); + const validUntil = BigInt(primary.validUntil); + const now = BigInt(Math.floor(Date.now() / 1000)); + const attestationStale = now > validUntil; + const minR = minReservesForM0(s0Sum); + const maxS1 = maxM1ForM0(s0Sum); + const m0Ok = r >= minR; + const m1Ok = s1Sum <= maxS1; + const policyOk = isCompliant(s0Sum, s1Sum, r); + + let operational = primary.operational; + const br651 = process.env.OMNL_CIRCUIT_BREAKER_651940; + if (br651) { + const p = new JsonRpcProvider(rpc651, 651940); + const b = new Contract(br651, BREAKER_ABI, p); + operational = operational && (await b.isLineOperational(lineId)); + } + + const reportingCompliant = policyOk && !attestationStale && operational; + + return { + aggregated: true, + chainId: 138, + lineId, + s0: s0Sum.toString(), + s1: s1Sum.toString(), + r: primary.r, + validUntil: primary.validUntil, + evidenceHash: primary.evidenceHash, + merkleRoot: primary.merkleRoot, + minR: minR.toString(), + maxS1: maxS1.toString(), + m0Ok, + m1Ok, + attestationStale, + policyOk, + operational, + reportingCompliant, + s0ByChain, + s1ByChain, + }; +} + +/** Latest commitment tuple from ReserveCommitmentStore (when OMNL_RESERVE_STORE_* set). */ +export async function fetchLatestAttestation( + chainId: number, + lineIdHex: string +): Promise<{ r: string; validUntil: string; evidenceHash: string; merkleRoot: string; version: string }> { + const addr = + chainId === 138 + ? process.env.OMNL_RESERVE_STORE_138 + : chainId === 651940 + ? process.env.OMNL_RESERVE_STORE_651940 + : process.env[`OMNL_RESERVE_STORE_${chainId}`]; + if (!addr) { + throw new Error(`OMNL_RESERVE_STORE_${chainId} not configured`); + } + const url = rpcForChain(chainId); + if (!url) throw new Error(`No RPC for ${chainId}`); + const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`; + const c = new Contract(addr, RESERVE_ABI, new JsonRpcProvider(url, chainId)); + const t = await c.getCommitment(lineId); + const x = t as unknown as { R: bigint; validUntil: bigint; evidenceHash: string; merkleRoot: string; version: bigint }; + return { + r: x.R.toString(), + validUntil: x.validUntil.toString(), + evidenceHash: x.evidenceHash, + merkleRoot: x.merkleRoot, + version: x.version.toString(), + }; +} + +export async function fetchBreakerStatus(chainId: number, lineIdHex: string): Promise<{ + globalPaused: boolean; + linePaused: boolean; + operational: boolean; +}> { + const addr = + chainId === 138 + ? process.env.OMNL_CIRCUIT_BREAKER_138 + : chainId === 651940 + ? process.env.OMNL_CIRCUIT_BREAKER_651940 + : process.env[`OMNL_CIRCUIT_BREAKER_${chainId}`]; + if (!addr) { + throw new Error(`OMNL_CIRCUIT_BREAKER_${chainId} not configured`); + } + const url = rpcForChain(chainId); + if (!url) throw new Error(`No RPC for ${chainId}`); + const lineId = lineIdHex.startsWith('0x') ? lineIdHex : `0x${lineIdHex}`; + const abi: InterfaceAbi = [ + 'function globalPaused() view returns (bool)', + 'function linePaused(bytes32) view returns (bool)', + 'function isLineOperational(bytes32 lineId) view returns (bool)', + ]; + const b = new Contract(addr, abi, new JsonRpcProvider(url, chainId)); + const [globalPaused, linePaused, operational] = await Promise.all([ + b.globalPaused(), + b.linePaused(lineId), + b.isLineOperational(lineId), + ]); + return { globalPaused, linePaused, operational }; +} diff --git a/services/token-aggregation/src/services/omnl-integration-status.ts b/services/token-aggregation/src/services/omnl-integration-status.ts new file mode 100644 index 0000000..8f3a730 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-integration-status.ts @@ -0,0 +1,31 @@ +import { loadIpsasRegistryPath } from './omnl-ipsas-gl'; +import { loadJournalMatrixPath } from './omnl-journal-matrix'; + +/** Non-secret snapshot of which OMNL integrations are configured (for probes and dashboards). */ +export function getOmnlIntegrationStatus(): Record { + return { + fineract: Boolean( + (process.env.OMNL_FINERACT_BASE_URL || '').trim() && (process.env.OMNL_FINERACT_PASSWORD || '').trim() + ), + complianceCore138: Boolean((process.env.OMNL_COMPLIANCE_CORE_138 || '').trim()), + complianceCore651940: Boolean((process.env.OMNL_COMPLIANCE_CORE_651940 || '').trim()), + instrumentRegistry138: Boolean( + (process.env.OMNL_INSTRUMENT_REGISTRY_138 || process.env.OMNL_INSTRUMENT_REGISTRY || '').trim() + ), + reserveStore138: Boolean((process.env.OMNL_RESERVE_STORE_138 || '').trim()), + reserveStore651940: Boolean((process.env.OMNL_RESERVE_STORE_651940 || '').trim()), + circuitBreaker138: Boolean((process.env.OMNL_CIRCUIT_BREAKER_138 || '').trim()), + circuitBreaker651940: Boolean((process.env.OMNL_CIRCUIT_BREAKER_651940 || '').trim()), + mirrorCoordinator138: Boolean( + (process.env.OMNL_MIRROR_COORDINATOR_138 || process.env.OMNL_MIRROR_COORDINATOR || '').trim() + ), + zkVerifierConfigured: Boolean((process.env.OMNL_ZK_VERIFIER || '').trim()), + webhooksConfigured: Boolean((process.env.OMNL_WEBHOOK_URLS || '').trim()), + eventPollerEnabled: ['1', 'true', 'yes', 'on'].includes( + (process.env.ENABLE_OMNL_EVENT_POLLER || '').trim().toLowerCase() + ), + apiKeyRequiredRoutes: Boolean((process.env.OMNL_API_KEY || '').trim()), + ipsasRegistryPath: loadIpsasRegistryPath(), + journalMatrixPath: loadJournalMatrixPath(), + }; +} diff --git a/services/token-aggregation/src/services/omnl-ipsas-gl.ts b/services/token-aggregation/src/services/omnl-ipsas-gl.ts new file mode 100644 index 0000000..8358292 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-ipsas-gl.ts @@ -0,0 +1,210 @@ +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; +import axios, { type AxiosRequestConfig } from 'axios'; +import type { JournalMatrixFile } from './omnl-journal-matrix'; + +export interface IpsasGlAccount { + glCode: string; + name: string; + fineractType: string; + usage: string; + ipsasStandards: string[]; + roles: string[]; +} + +export interface AllowedJournalPair { + debitGlCode: string; + creditGlCode: string; + ipsasRef: string; + memo?: string; +} + +export interface IpsasGlRegistry { + version: string; + currencyCode: string; + accounts: IpsasGlAccount[]; + allowedJournalPairs: AllowedJournalPair[]; + monetaryLayerHints: Record< + string, + { primaryGlCodes: string[]; ipsasNarrative: string } + >; +} + +export function loadIpsasRegistryPath(): string { + return ( + process.env.OMNL_IPSAS_GL_REGISTRY || + resolve(__dirname, '../../../../config/omnl-ipsas-gl-registry.json') + ); +} + +export function loadIpsasRegistry(): IpsasGlRegistry { + const p = loadIpsasRegistryPath(); + if (!existsSync(p)) { + throw new Error(`IPSAS GL registry not found: ${p}`); + } + return JSON.parse(readFileSync(p, 'utf8')) as IpsasGlRegistry; +} + +export function validateJournalPair( + registry: IpsasGlRegistry, + debitGlCode: string, + creditGlCode: string +): { valid: boolean; ipsasRef?: string; memo?: string } { + const d = debitGlCode.trim(); + const c = creditGlCode.trim(); + const hit = registry.allowedJournalPairs.find( + (p) => p.debitGlCode === d && p.creditGlCode === c + ); + if (hit) { + return { valid: true, ipsasRef: hit.ipsasRef, memo: hit.memo }; + } + return { valid: false }; +} + +/** Prefer static registry; fall back to full journal matrix (T-001…T-008). */ +export function validateJournalPairWithMatrix( + registry: IpsasGlRegistry, + matrix: JournalMatrixFile | null, + debitGlCode: string, + creditGlCode: string +): { valid: boolean; ipsasRef?: string; memo?: string; narrative?: string; source: 'registry' | 'matrix' | 'none' } { + const r0 = validateJournalPair(registry, debitGlCode, creditGlCode); + if (r0.valid) { + return { ...r0, source: 'registry' }; + } + const d = debitGlCode.trim(); + const c = creditGlCode.trim(); + if (matrix) { + const hit = matrix.entries.find((e) => e.debitGlCode === d && e.creditGlCode === c); + if (hit) { + return { + valid: true, + ipsasRef: hit.ipsasRef, + memo: hit.memo, + narrative: hit.narrative, + source: 'matrix', + }; + } + } + return { valid: false, source: 'none' }; +} + +export function assertGlCodeKnown(registry: IpsasGlRegistry, glCode: string): boolean { + return registry.accounts.some((a) => a.glCode === glCode.trim()); +} + +export interface FineractGlAccountRow { + id: number; + glCode?: string; + name?: string; +} + +/** GET /glaccounts from Fineract (OMNL). Paginates until exhausted; handles top-level array or pageItems. */ +export async function fetchFineractGlAccounts(): Promise { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl'; + const user = + process.env.OMNL_FINERACT_USER?.trim() || + process.env.OMNL_FINERACT_USERNAME?.trim() || + 'app.omnl'; + const pass = process.env.OMNL_FINERACT_PASSWORD || ''; + if (!base || !pass) { + throw new Error('OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD required for Fineract GL sync'); + } + const auth = Buffer.from(`${user}:${pass}`).toString('base64'); + const headers: Record = { + Authorization: `Basic ${auth}`, + 'Fineract-Platform-TenantId': tenant, + 'Content-Type': 'application/json', + }; + const limit = parseInt(process.env.OMNL_FINERACT_GL_PAGE_LIMIT || '200', 10); + const out: FineractGlAccountRow[] = []; + let offset = 0; + for (;;) { + const url = `${base}/glaccounts?limit=${limit}&offset=${offset}`; + const cfg: AxiosRequestConfig = { headers, timeout: 120000 }; + const { data } = await axios.get< + FineractGlAccountRow[] | { pageItems?: FineractGlAccountRow[]; totalFilteredRecords?: number } + >(url, cfg); + let batch: FineractGlAccountRow[] = []; + if (Array.isArray(data)) { + batch = data; + } else { + batch = data.pageItems ?? []; + } + out.push(...batch); + if (batch.length < limit) { + break; + } + offset += limit; + } + return out; +} + +export function compareRegistryToFineract( + registry: IpsasGlRegistry, + fineractRows: FineractGlAccountRow[] +): { + registryCodes: string[]; + fineractCodes: string[]; + missingInFineract: string[]; + aligned: boolean; +} { + const registryCodes = registry.accounts.map((a) => a.glCode).sort(); + const fineractCodes = [ + ...new Set( + fineractRows + .map((r) => (r.glCode != null ? String(r.glCode).trim() : '')) + .filter(Boolean) + ), + ].sort(); + const fs = new Set(fineractCodes); + const missingInFineract = registryCodes.filter((c) => !fs.has(c)); + return { + registryCodes, + fineractCodes, + missingInFineract, + aligned: missingInFineract.length === 0, + }; +} + +/** Lightweight Fineract reachability (GET `/glaccounts?limit=1`). */ +export async function checkFineractConnectivity(): Promise<{ + ok: boolean; + statusCode?: number; + message: string; +}> { + const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); + const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl'; + const user = + process.env.OMNL_FINERACT_USER?.trim() || + process.env.OMNL_FINERACT_USERNAME?.trim() || + 'app.omnl'; + const pass = process.env.OMNL_FINERACT_PASSWORD || ''; + if (!base || !pass) { + return { ok: false, message: 'OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD required' }; + } + const auth = Buffer.from(`${user}:${pass}`).toString('base64'); + const headers: Record = { + Authorization: `Basic ${auth}`, + 'Fineract-Platform-TenantId': tenant, + 'Content-Type': 'application/json', + }; + try { + const url = `${base}/glaccounts?limit=1&offset=0`; + const { status } = await axios.get(url, { + headers, + timeout: 20000, + validateStatus: () => true, + }); + const ok = status >= 200 && status < 500; + return { + ok, + statusCode: status, + message: ok ? 'Fineract API reachable' : `Unexpected HTTP ${status}`, + }; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { ok: false, message: msg }; + } +} diff --git a/services/token-aggregation/src/services/omnl-journal-matrix.ts b/services/token-aggregation/src/services/omnl-journal-matrix.ts new file mode 100644 index 0000000..a49590c --- /dev/null +++ b/services/token-aggregation/src/services/omnl-journal-matrix.ts @@ -0,0 +1,43 @@ +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; + +export interface JournalMatrixEntry { + memo: string; + officeId: number; + debitGlCode: string; + creditGlCode: string; + amount: number; + narrative: string; + ipsasRef: string; +} + +export interface JournalMatrixFile { + entries: JournalMatrixEntry[]; + description?: string; + source?: string; + currencyCode?: string; +} + +export function loadJournalMatrixPath(): string { + return ( + process.env.OMNL_JOURNAL_MATRIX_PATH || + resolve(__dirname, '../../../../config/omnl-journal-matrix.json') + ); +} + +export function loadJournalMatrix(): JournalMatrixFile { + const p = loadJournalMatrixPath(); + if (!existsSync(p)) { + throw new Error(`Journal matrix not found: ${p}. Set OMNL_JOURNAL_MATRIX_PATH.`); + } + return JSON.parse(readFileSync(p, 'utf8')) as JournalMatrixFile; +} + +/** Union of IPSAS registry pairs and matrix-derived pairs for validation. */ +export function matrixPairSet(matrix: JournalMatrixFile): Set { + const s = new Set(); + for (const e of matrix.entries) { + s.add(`${e.debitGlCode}|${e.creditGlCode}`); + } + return s; +} diff --git a/services/token-aggregation/src/services/omnl-policy-math.ts b/services/token-aggregation/src/services/omnl-policy-math.ts new file mode 100644 index 0000000..6196be5 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-policy-math.ts @@ -0,0 +1,19 @@ +/** + * Deterministic GRU policy (must match PolicyMath.sol / HYBX_OMNL_POLICY_SPEC). + */ +const M0_NUM = 12n; +const M0_DEN = 10n; +const M1_CAP = 5n; + +export function minReservesForM0(s0: bigint): bigint { + if (s0 === 0n) return 0n; + return (s0 * M0_NUM + M0_DEN - 1n) / M0_DEN; +} + +export function maxM1ForM0(s0: bigint): bigint { + return s0 * M1_CAP; +} + +export function isCompliant(s0: bigint, s1: bigint, r: bigint): boolean { + return r >= minReservesForM0(s0) && s1 <= maxM1ForM0(s0); +} diff --git a/services/token-aggregation/src/services/omnl-reconcile-anchor.test.ts b/services/token-aggregation/src/services/omnl-reconcile-anchor.test.ts new file mode 100644 index 0000000..bbf4433 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-reconcile-anchor.test.ts @@ -0,0 +1,11 @@ +import { computeOmnlReconcileAnchor } from './omnl-reconcile-anchor'; + +describe('computeOmnlReconcileAnchor', () => { + it('returns ok and 64-char hex sha256 when config files exist', () => { + const r = computeOmnlReconcileAnchor(); + expect(r.ok).toBe(true); + expect(r.sha256).toMatch(/^[a-f0-9]{64}$/); + expect(r.registryPath).toContain('omnl-ipsas-gl-registry'); + expect(r.matrixPath).toContain('omnl-journal-matrix'); + }); +}); diff --git a/services/token-aggregation/src/services/omnl-reconcile-anchor.ts b/services/token-aggregation/src/services/omnl-reconcile-anchor.ts new file mode 100644 index 0000000..731cd2e --- /dev/null +++ b/services/token-aggregation/src/services/omnl-reconcile-anchor.ts @@ -0,0 +1,60 @@ +import { readFileSync, existsSync } from 'fs'; +import { createHash } from 'crypto'; +import { loadIpsasRegistryPath } from './omnl-ipsas-gl'; +import { loadJournalMatrixPath } from './omnl-journal-matrix'; + +export interface OmnlReconcileAnchorResult { + ok: boolean; + sha256: string; + registryPath: string; + matrixPath: string; + generatedAt: string; + error?: string; +} + +/** Same canonical SHA-256 as `scripts/omnl-reconcile-report.mjs` (IPSAS registry + journal matrix JSON). */ +export function computeOmnlReconcileAnchor(): OmnlReconcileAnchorResult { + const generatedAt = new Date().toISOString(); + try { + const registryPath = loadIpsasRegistryPath(); + const matrixPath = loadJournalMatrixPath(); + if (!existsSync(registryPath)) { + return { + ok: false, + sha256: '', + registryPath, + matrixPath, + generatedAt, + error: `IPSAS registry file missing: ${registryPath}`, + }; + } + if (!existsSync(matrixPath)) { + return { + ok: false, + sha256: '', + registryPath, + matrixPath, + generatedAt, + error: `Journal matrix file missing: ${matrixPath}`, + }; + } + const reg = readFileSync(registryPath, 'utf8'); + const matrix = readFileSync(matrixPath, 'utf8'); + const canonical = JSON.stringify({ + registry: JSON.parse(reg) as unknown, + matrix: JSON.parse(matrix) as unknown, + }); + const sha256 = createHash('sha256').update(canonical).digest('hex'); + return { ok: true, sha256, registryPath, matrixPath, generatedAt }; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + return { + ok: false, + sha256: '', + registryPath: '', + matrixPath: '', + generatedAt, + error: msg, + }; + } +} diff --git a/services/token-aggregation/src/services/omnl-webhooks.test.ts b/services/token-aggregation/src/services/omnl-webhooks.test.ts new file mode 100644 index 0000000..37b796c --- /dev/null +++ b/services/token-aggregation/src/services/omnl-webhooks.test.ts @@ -0,0 +1,18 @@ +import { signOmnlWebhookBody, verifyOmnlWebhookSignature } from './omnl-webhooks'; + +describe('omnl-webhooks', () => { + it('signOmnlWebhookBody produces sha256= prefix', () => { + const raw = '{"event":"ReserveCommitted","ts":"2026-01-01T00:00:00.000Z"}'; + const sig = signOmnlWebhookBody(raw, 'secret'); + expect(sig.startsWith('sha256=')).toBe(true); + expect(sig.length).toBeGreaterThan(10); + }); + + it('verifyOmnlWebhookSignature accepts matching signature', () => { + const raw = '{"a":1}'; + const secret = 'k'; + const sig = signOmnlWebhookBody(raw, secret); + expect(verifyOmnlWebhookSignature(raw, sig, secret)).toBe(true); + expect(verifyOmnlWebhookSignature(raw, 'sha256=deadbeef', secret)).toBe(false); + }); +}); diff --git a/services/token-aggregation/src/services/omnl-webhooks.ts b/services/token-aggregation/src/services/omnl-webhooks.ts new file mode 100644 index 0000000..68d7cd3 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-webhooks.ts @@ -0,0 +1,69 @@ +import axios from 'axios'; +import { createHmac, timingSafeEqual } from 'crypto'; +import { logger } from '../utils/logger'; + +export type OmnlWebhookPayload = { + event: string; + chainId?: number; + lineId?: string; + /** Idempotency: `${chainId}-${blockNumber}-${logIndex}` */ + deliveryId?: string; + payload: Record; + ts: string; +}; + +function parseWebhookUrls(): string[] { + const raw = process.env.OMNL_WEBHOOK_URLS || ''; + return raw + .split(',') + .map((s) => s.trim()) + .filter(Boolean); +} + +/** HMAC-SHA256(hex) of UTF-8 body; header value `sha256=`. */ +export function signOmnlWebhookBody(rawBodyUtf8: string, secret: string): string { + const h = createHmac('sha256', secret).update(rawBodyUtf8, 'utf8').digest('hex'); + return `sha256=${h}`; +} + +/** + * Verify `X-OMNL-Signature` from `signOmnlWebhookBody` (timing-safe). + */ +export function verifyOmnlWebhookSignature(rawBodyUtf8: string, signatureHeader: string | undefined, secret: string): boolean { + if (!signatureHeader || !secret) return false; + const expected = signOmnlWebhookBody(rawBodyUtf8, secret); + try { + const a = Buffer.from(signatureHeader.trim(), 'utf8'); + const b = Buffer.from(expected, 'utf8'); + if (a.length !== b.length) return false; + return timingSafeEqual(a, b); + } catch { + return false; + } +} + +/** + * POST JSON to each configured OMNL_WEBHOOK_URLS (comma-separated). Failures are logged, not thrown. + * Sends `X-OMNL-Signature: sha256=` when OMNL_WEBHOOK_SECRET is set (HMAC of exact JSON body bytes). + */ +export async function emitOmnlWebhook(body: OmnlWebhookPayload): Promise { + const urls = parseWebhookUrls(); + if (urls.length === 0) { + return; + } + const secret = process.env.OMNL_WEBHOOK_SECRET || ''; + const rawBody = JSON.stringify(body); + const headers: Record = { + 'Content-Type': 'application/json; charset=utf-8', + ...(secret ? { 'X-OMNL-Signature': signOmnlWebhookBody(rawBody, secret) } : {}), + }; + await Promise.allSettled( + urls.map(async (url) => { + try { + await axios.post(url, rawBody, { timeout: 15000, headers }); + } catch (e) { + logger.warn(`OMNL webhook failed ${url}`, { error: e instanceof Error ? e.message : String(e) }); + } + }) + ); +} diff --git a/test/flash/EstimateMainnetCwUnwindFork.t.sol b/test/flash/EstimateMainnetCwUnwindFork.t.sol new file mode 100644 index 0000000..dad06e8 --- /dev/null +++ b/test/flash/EstimateMainnetCwUnwindFork.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {DODOIntegrationExternalUnwinder} from "../../contracts/flash/DODOIntegrationExternalUnwinder.sol"; + +contract EstimateMainnetCwUnwindForkTest is Test { + address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84; + address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E; + address constant POOL_CWUSDT_USDT = 0x79156F6B7bf71a1B72D78189B540A89A6C13F6FC; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a; + address constant CWUSDT = 0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE; + + DODOIntegrationExternalUnwinder internal unwinder; + + function setUp() public { + string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC"); + vm.createSelectFork(rpcUrl); + unwinder = new DODOIntegrationExternalUnwinder(DODO_PMM_INTEGRATION_MAINNET); + } + + function testEstimateDirectUnwinds() public { + uint256[] memory sizes = new uint256[](10); + sizes[0] = 100_000; // 0.1 + sizes[1] = 1_000_000; // 1 + sizes[2] = 5_000_000; // 5 + sizes[3] = 10_000_000; // 10 + sizes[4] = 25_000_000; // 25 + sizes[5] = 50_000_000; // 50 + sizes[6] = 100_000_000; // 100 + sizes[7] = 250_000_000; // 250 + sizes[8] = 500_000_000; // 500 + sizes[9] = 1_000_000_000; // 1000 + + console2.log("cWUSDC -> USDC"); + _logCurve(CWUSDC, USDC, POOL_CWUSDC_USDC, sizes); + + console2.log("cWUSDT -> USDT"); + _logCurve(CWUSDT, USDT, POOL_CWUSDT_USDT, sizes); + } + + function _logCurve(address tokenIn, address tokenOut, address pool, uint256[] memory sizes) internal { + for (uint256 i = 0; i < sizes.length; i++) { + uint256 snap = vm.snapshotState(); + uint256 amountIn = sizes[i]; + deal(tokenIn, address(this), amountIn); + IERC20(tokenIn).approve(address(unwinder), amountIn); + try unwinder.unwind(tokenIn, tokenOut, amountIn, 1, abi.encode(pool)) returns (uint256 amountOut) { + console2.log("in_raw", amountIn, "out_raw", amountOut); + } catch { + console2.log("in_raw", amountIn, "out_raw", uint256(0)); + } + vm.revertToState(snap); + } + } +} diff --git a/test/hybx-omnl/ComplianceCore.t.sol b/test/hybx-omnl/ComplianceCore.t.sol new file mode 100644 index 0000000..4393bab --- /dev/null +++ b/test/hybx-omnl/ComplianceCore.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {InstrumentRegistry} from "../../contracts/hybx-omnl/InstrumentRegistry.sol"; +import {ReserveCommitmentStore} from "../../contracts/hybx-omnl/ReserveCommitmentStore.sol"; +import {OMNLCircuitBreaker} from "../../contracts/hybx-omnl/OMNLCircuitBreaker.sol"; +import {ComplianceCore} from "../../contracts/hybx-omnl/ComplianceCore.sol"; +import {MockMintableToken} from "../dbis/MockMintableToken.sol"; + +contract ComplianceCoreTest is Test { + InstrumentRegistry public registry; + ReserveCommitmentStore public reserveStore; + OMNLCircuitBreaker public breakers; + ComplianceCore public core; + + MockMintableToken public m0; + MockMintableToken public m1; + + bytes32 public constant LINE = keccak256("TEST_USD"); + + function setUp() public { + address admin = address(this); + registry = new InstrumentRegistry(admin); + reserveStore = new ReserveCommitmentStore(admin); + breakers = new OMNLCircuitBreaker(admin); + core = new ComplianceCore(address(registry), address(reserveStore), address(breakers)); + + m0 = new MockMintableToken("M0", "M0", 6, admin); + m1 = new MockMintableToken("M1", "M1", 6, admin); + + registry.registerLine(LINE, address(m0), address(m1), 6, 840, false); + + reserveStore.commitReserve(LINE, 130e6, block.timestamp + 1 days, bytes32(uint256(1)), bytes32(uint256(42))); + + m0.mint(address(this), 100e6); + m1.mint(address(this), 400e6); + } + + function testGetCompliance() public view { + ( + uint256 s0, + uint256 s1, + uint256 r, + , + bytes32 evidenceHash, + bytes32 merkleRoot, + uint256 minR, + uint256 maxS1, + bool m0Ok, + bool m1Ok, + bool stale, + bool policyOk, + bool operational, + bool reportingCompliant + ) = core.getCompliance(LINE); + + assertEq(s0, 100e6); + assertEq(s1, 400e6); + assertEq(r, 130e6); + assertEq(evidenceHash, bytes32(uint256(1))); + assertEq(merkleRoot, bytes32(uint256(42))); + assertEq(minR, 120e6); + assertEq(maxS1, 500e6); + assertTrue(m0Ok); + assertTrue(m1Ok); + assertFalse(stale); + assertTrue(policyOk); + assertTrue(operational); + assertTrue(reportingCompliant); + } + + function testReportingFalseWhenStale() public { + vm.warp(block.timestamp + 2 days); + (,,,,,,,,,, bool stale,,, bool reportingCompliant) = core.getCompliance(LINE); + assertTrue(stale); + assertFalse(reportingCompliant); + } + + function testAssertMintM1FailsOverCap() public { + vm.expectRevert(abi.encodeWithSelector(ComplianceCore.ComplianceBlocked.selector, LINE, "m1_cap")); + core.assertCanMintM1(LINE, 101e6); + } + + function testAssertMintM0FailsInsufficientR() public { + vm.expectRevert(abi.encodeWithSelector(ComplianceCore.ComplianceBlocked.selector, LINE, "m0_backing")); + core.assertCanMintM0(LINE, 11e6); + } +} diff --git a/test/hybx-omnl/PolicyMath.t.sol b/test/hybx-omnl/PolicyMath.t.sol new file mode 100644 index 0000000..d94d304 --- /dev/null +++ b/test/hybx-omnl/PolicyMath.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {PolicyMath} from "../../contracts/hybx-omnl/PolicyMath.sol"; + +contract PolicyMathTest is Test { + function testMinReservesCeil() public pure { + // ceil(1.2 * 10) = 12 + assertEq(PolicyMath.minReservesForM0(10), 12); + // ceil(1.2 * 1) = 2 (1.2 rounded up) + assertEq(PolicyMath.minReservesForM0(1), 2); + } + + function testMaxM1() public pure { + assertEq(PolicyMath.maxM1ForM0(100), 500); + } + + function testCompliant() public pure { + assertTrue(PolicyMath.isCompliant(100, 400, 120)); + assertFalse(PolicyMath.isCompliant(100, 600, 120)); + assertFalse(PolicyMath.isCompliant(100, 400, 119)); + } +} diff --git a/test/hybx-omnl/ReserveAttestation.t.sol b/test/hybx-omnl/ReserveAttestation.t.sol new file mode 100644 index 0000000..1a4bbc0 --- /dev/null +++ b/test/hybx-omnl/ReserveAttestation.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ReserveCommitmentStore} from "../../contracts/hybx-omnl/ReserveCommitmentStore.sol"; + +contract ReserveAttestationTest is Test { + uint256 internal constant PK = 0xA11CE; + + ReserveCommitmentStore public store; + bytes32 internal constant LINE = keccak256("ATTEST_LINE"); + + function setUp() public { + store = new ReserveCommitmentStore(address(this)); + address signer = vm.addr(PK); + store.setAttestationSigner(signer, true); + store.setAttestationThreshold(1); + } + + function testCommitReserveAttested() public { + uint256 R = 1_000_000; + uint256 validUntil = block.timestamp + 1 days; + bytes32 ev = bytes32(uint256(7)); + bytes32 mr = bytes32(uint256(8)); + uint256 nonce = 0; + + bytes32 digest = keccak256( + abi.encode( + store.ATTESTATION_TYPEHASH(), + block.chainid, + address(store), + LINE, + R, + validUntil, + ev, + mr, + nonce + ) + ); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(PK, ethSignedMessageHash); + bytes[] memory sigs = new bytes[](1); + sigs[0] = abi.encodePacked(r, s, v); + + vm.prank(address(0xBEEF)); + store.commitReserveAttested(LINE, R, validUntil, ev, mr, nonce, sigs); + + ReserveCommitmentStore.Commitment memory c = store.getCommitment(LINE); + assertEq(c.R, R); + assertEq(c.merkleRoot, mr); + assertEq(store.lineAttestationNonce(LINE), 1); + } +}