From a1293e8445ad8f9dafd5471874d222d40f6aacec Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 14 Feb 2026 23:40:37 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20Genex=20Chain=20=E5=8C=BA=E5=9D=97?= =?UTF-8?q?=E9=93=BE=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0=20=E2=80=94=20cos?= =?UTF-8?q?mos/evm=20v0.5.1=20=E5=BA=94=E7=94=A8=E9=93=BE=20+=209=E5=90=88?= =?UTF-8?q?=E7=BA=A6=20+=20=E5=90=88=E8=A7=84=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 区块链核心 (blockchain/genex-chain/): - app.go: 真实 Cosmos SDK BaseApp, 20+ 模块注册 (auth/bank/staking/evm/feemarket/erc20/ibc...) - genesis.go: EVM/FeeMarket/Mint 创世状态, NoBaseFee=true (Gas补贴) - compliance_integration.go: ComplianceAnteHandler 桥接到 SDK ante 链 TX → OFAC/TravelRule/Structuring 拦截 → cosmos/evm AnteHandler → Mempool - cmd/genexd/: 完整 CLI (init/start/keys/query/tx + EVM JSON-RPC/WS) - x/evm/ante/: 链级合规拦截 (OFAC + Travel Rule ≥$3k + Structuring 24h检测), 18 tests - x/evm/keeper/: Gas 费覆盖 (平台补贴 + EIP-1559 预留), 13 tests 智能合约 (blockchain/genex-contracts/): - 9 合约: CouponFactory, Coupon(ERC-721), Settlement, Redemption, Compliance, Treasury, Governance, ExchangeRateOracle, CouponBackedSecurity - 3 接口: ICoupon, ICompliance, IChainlinkPriceFeed - Deploy.s.sol: Transparent Proxy 部署 + 角色授权 - 102/102 Foundry tests (含 fuzz + 集成) 链参数: Bech32=genex, Denom=agnx(18d), EVM ChainID=8888, ≤1s出块 部署: Docker 6节点 (3创世+2机构+1监管), docker-compose.yml 修复: - ComplianceAnteHandler 集成到真实 Cosmos SDK ante handler 链 (§6/§16) - init-testnet.sh denom 从 ugnx 修正为 agnx + 18位精度金额 (§13) 指南符合性: 06-区块链开发指南.md 21节中17节完全符合, 4节偏差已修复/标注 Co-Authored-By: Claude Opus 4.6 --- .claude/blockchain-dev-plan.md | 273 +++++ blockchain/README.md | 131 +++ blockchain/docker-compose.yml | 191 ++++ blockchain/genex-chain/.dockerignore | 9 + blockchain/genex-chain/Dockerfile | 90 ++ blockchain/genex-chain/Makefile | 127 +++ blockchain/genex-chain/app.go | 978 ++++++++++++++++++ blockchain/genex-chain/cmd/genexd/cmd/root.go | 372 +++++++ blockchain/genex-chain/cmd/genexd/main.go | 40 + .../genex-chain/compliance_integration.go | 149 +++ blockchain/genex-chain/config/app.toml | 178 ++++ blockchain/genex-chain/config/config.toml | 141 +++ blockchain/genex-chain/config/genesis.json | 182 ++++ blockchain/genex-chain/export.go | 209 ++++ blockchain/genex-chain/genesis.go | 36 + blockchain/genex-chain/go.mod | 40 + blockchain/genex-chain/interfaces.go | 12 + blockchain/genex-chain/mempool.go | 69 ++ .../genex-chain/scripts/build-production.sh | 126 +++ blockchain/genex-chain/scripts/init-local.sh | 137 +++ .../genex-chain/scripts/init-testnet.sh | 186 ++++ blockchain/genex-chain/upgrades.go | 31 + .../genex-chain/x/evm/ante/compliance_ante.go | 240 +++++ .../x/evm/ante/compliance_ante_test.go | 302 ++++++ blockchain/genex-chain/x/evm/keeper/gas.go | 117 +++ .../genex-chain/x/evm/keeper/gas_test.go | 157 +++ blockchain/genex-contracts/.gitignore | 10 + blockchain/genex-contracts/.gitmodules | 9 + blockchain/genex-contracts/Dockerfile | 71 ++ blockchain/genex-contracts/foundry.lock | 20 + blockchain/genex-contracts/foundry.toml | 24 + blockchain/genex-contracts/remappings.txt | 2 + .../genex-contracts/script/Deploy.s.sol | 198 ++++ blockchain/genex-contracts/src/Compliance.sol | 231 +++++ blockchain/genex-contracts/src/Coupon.sol | 139 +++ .../src/CouponBackedSecurity.sol | 228 ++++ .../genex-contracts/src/CouponFactory.sol | 145 +++ .../src/ExchangeRateOracle.sol | 100 ++ blockchain/genex-contracts/src/Governance.sol | 213 ++++ blockchain/genex-contracts/src/Redemption.sol | 106 ++ blockchain/genex-contracts/src/Settlement.sol | 182 ++++ blockchain/genex-contracts/src/Treasury.sol | 186 ++++ .../src/interfaces/IChainlinkPriceFeed.sol | 20 + .../src/interfaces/ICompliance.sol | 33 + .../src/interfaces/ICoupon.sol | 30 + .../genex-contracts/test/Compliance.t.sol | 194 ++++ blockchain/genex-contracts/test/Coupon.t.sol | 157 +++ .../test/CouponBackedSecurity.t.sol | 259 +++++ .../genex-contracts/test/CouponFactory.t.sol | 180 ++++ .../test/ExchangeRateOracle.t.sol | 126 +++ .../genex-contracts/test/Governance.t.sol | 172 +++ .../genex-contracts/test/Integration.t.sol | 302 ++++++ .../genex-contracts/test/Redemption.t.sol | 145 +++ .../genex-contracts/test/Settlement.t.sol | 192 ++++ .../genex-contracts/test/Treasury.t.sol | 187 ++++ 55 files changed, 8384 insertions(+) create mode 100644 .claude/blockchain-dev-plan.md create mode 100644 blockchain/README.md create mode 100644 blockchain/docker-compose.yml create mode 100644 blockchain/genex-chain/.dockerignore create mode 100644 blockchain/genex-chain/Dockerfile create mode 100644 blockchain/genex-chain/Makefile create mode 100644 blockchain/genex-chain/app.go create mode 100644 blockchain/genex-chain/cmd/genexd/cmd/root.go create mode 100644 blockchain/genex-chain/cmd/genexd/main.go create mode 100644 blockchain/genex-chain/compliance_integration.go create mode 100644 blockchain/genex-chain/config/app.toml create mode 100644 blockchain/genex-chain/config/config.toml create mode 100644 blockchain/genex-chain/config/genesis.json create mode 100644 blockchain/genex-chain/export.go create mode 100644 blockchain/genex-chain/genesis.go create mode 100644 blockchain/genex-chain/go.mod create mode 100644 blockchain/genex-chain/interfaces.go create mode 100644 blockchain/genex-chain/mempool.go create mode 100644 blockchain/genex-chain/scripts/build-production.sh create mode 100644 blockchain/genex-chain/scripts/init-local.sh create mode 100644 blockchain/genex-chain/scripts/init-testnet.sh create mode 100644 blockchain/genex-chain/upgrades.go create mode 100644 blockchain/genex-chain/x/evm/ante/compliance_ante.go create mode 100644 blockchain/genex-chain/x/evm/ante/compliance_ante_test.go create mode 100644 blockchain/genex-chain/x/evm/keeper/gas.go create mode 100644 blockchain/genex-chain/x/evm/keeper/gas_test.go create mode 100644 blockchain/genex-contracts/.gitignore create mode 100644 blockchain/genex-contracts/.gitmodules create mode 100644 blockchain/genex-contracts/Dockerfile create mode 100644 blockchain/genex-contracts/foundry.lock create mode 100644 blockchain/genex-contracts/foundry.toml create mode 100644 blockchain/genex-contracts/remappings.txt create mode 100644 blockchain/genex-contracts/script/Deploy.s.sol create mode 100644 blockchain/genex-contracts/src/Compliance.sol create mode 100644 blockchain/genex-contracts/src/Coupon.sol create mode 100644 blockchain/genex-contracts/src/CouponBackedSecurity.sol create mode 100644 blockchain/genex-contracts/src/CouponFactory.sol create mode 100644 blockchain/genex-contracts/src/ExchangeRateOracle.sol create mode 100644 blockchain/genex-contracts/src/Governance.sol create mode 100644 blockchain/genex-contracts/src/Redemption.sol create mode 100644 blockchain/genex-contracts/src/Settlement.sol create mode 100644 blockchain/genex-contracts/src/Treasury.sol create mode 100644 blockchain/genex-contracts/src/interfaces/IChainlinkPriceFeed.sol create mode 100644 blockchain/genex-contracts/src/interfaces/ICompliance.sol create mode 100644 blockchain/genex-contracts/src/interfaces/ICoupon.sol create mode 100644 blockchain/genex-contracts/test/Compliance.t.sol create mode 100644 blockchain/genex-contracts/test/Coupon.t.sol create mode 100644 blockchain/genex-contracts/test/CouponBackedSecurity.t.sol create mode 100644 blockchain/genex-contracts/test/CouponFactory.t.sol create mode 100644 blockchain/genex-contracts/test/ExchangeRateOracle.t.sol create mode 100644 blockchain/genex-contracts/test/Governance.t.sol create mode 100644 blockchain/genex-contracts/test/Integration.t.sol create mode 100644 blockchain/genex-contracts/test/Redemption.t.sol create mode 100644 blockchain/genex-contracts/test/Settlement.t.sol create mode 100644 blockchain/genex-contracts/test/Treasury.t.sol diff --git a/.claude/blockchain-dev-plan.md b/.claude/blockchain-dev-plan.md new file mode 100644 index 0000000..3d7ce9e --- /dev/null +++ b/.claude/blockchain-dev-plan.md @@ -0,0 +1,273 @@ +# Genex Chain 区块链开发计划 + +> 基于 `docs/guides/06-区块链开发指南.md` v3.0 量产版 +> 技术栈: cosmos/evm v0.5.1 + Cosmos SDK v0.53.5 + CometBFT v0.38.19 + Solidity + Foundry + +--- + +## 目录结构 + +``` +blockchain/ +├── genex-chain/ # Cosmos SDK 应用链(Go) +│ ├── app.go # GenexApp (BaseApp) 主配置 — 真实 Cosmos SDK +│ ├── genesis.go # 创世状态 (EVM/FeeMarket/Mint) +│ ├── export.go # 状态导出 +│ ├── interfaces.go # BankKeeper 接口 +│ ├── mempool.go # EVM 内存池配置 +│ ├── upgrades.go # 升级处理器 +│ ├── cmd/ +│ │ └── genexd/ # 链二进制入口 +│ │ ├── main.go # 入口 + SDK 配置 +│ │ └── cmd/ +│ │ └── root.go # Cosmos SDK server 命令 +│ ├── x/ +│ │ └── evm/ +│ │ ├── keeper/ +│ │ │ ├── gas.go # Gas费覆盖(平台补贴) +│ │ │ └── gas_test.go # Gas 测试(13 tests) +│ │ └── ante/ +│ │ ├── compliance_ante.go # 链级合规拦截 +│ │ └── compliance_ante_test.go # 合规测试(18 tests) +│ ├── config/ +│ │ ├── config.toml # CometBFT 节点配置 +│ │ ├── app.toml # 应用配置 +│ │ └── genesis.json # 创世配置(参考) +│ ├── scripts/ +│ │ ├── init-local.sh # 本地测试链初始化 +│ │ ├── init-testnet.sh # 测试网初始化(5验证+1监管) +│ │ └── build-production.sh # 生产构建脚本 +│ ├── go.mod # 精确依赖 + replace 指令 +│ ├── Makefile # 构建系统 +│ ├── Dockerfile # 多阶段 Docker 构建 +│ └── .dockerignore +│ +├── genex-contracts/ # Solidity 智能合约(Foundry) +│ ├── src/ # 9 个合约 +│ ├── test/ # 102 tests +│ ├── script/Deploy.s.sol +│ ├── foundry.toml +│ └── Dockerfile +│ +├── docker-compose.yml # 完整链部署(6节点+合约) +└── README.md +``` + +--- + +## 核心技术栈 + +| 组件 | 版本 | 说明 | +|------|------|------| +| cosmos/evm | v0.5.1 | EVM 兼容层 (Apache 2.0) | +| Cosmos SDK | v0.53.5 (commit: 768cb210885c) | 应用链框架 | +| CometBFT | v0.38.19 | BFT 共识引擎 | +| ibc-go | v10.3.1 | IBC 跨链 | +| go-ethereum | v1.16.2-cosmos-1 (Cosmos fork) | EVM 实现 | +| OpenZeppelin | v4.9.6 | Solidity 合约库 | +| Foundry | latest | 合约开发框架 | + +## Genex Chain 参数 + +| 参数 | 值 | +|------|------| +| Bech32 Prefix | `genex` | +| Bond Denom | `agnx` (atto GNX, 18 decimals) | +| Display Denom | `GNX` | +| EVM Chain ID | 8888 | +| Min Gas Price | 0 (平台补贴) | +| Block Time | ≤1s | +| 节点 Home | `~/.genexd` | + +--- + +## 开发阶段与验证状态 + +### 阶段 1: 智能合约核心 ✅ +- [x] Foundry 工程 + OpenZeppelin v4.9.6 +- [x] 接口定义: ICoupon, ICompliance, IChainlinkPriceFeed +- [x] CouponFactory.sol — Utility/Security 双轨铸造 +- [x] Coupon.sol — ERC-721 + 不可转让 + 转售计数 +- **验证**: `forge build` 编译通过 + +### 阶段 2: 业务逻辑合约 ✅ +- [x] Settlement.sol — 原子交换 + 多稳定币 + 价格上限 + 退款 +- [x] Redemption.sol — 销毁 + 门店验证 + 到期检查 +- [x] Treasury.sol — 保障资金 + Escrow + 费用分割 + +### 阶段 3: 合规与治理合约 ✅ +- [x] Compliance.sol — OFAC + Travel Rule + KYC L0-L3 + 冻结 +- [x] Governance.sol — 3/5多签 + 48h/4h时间锁 + 回滚 +- [x] ExchangeRateOracle.sol — Chainlink + 过期保护 +- [x] CouponBackedSecurity.sol — CBS池 + 份额 + 收益分配 + +### 阶段 4: 部署脚本 ✅ +- [x] Deploy.s.sol — Transparent Proxy + 角色授权 + +### 阶段 5: Solidity 测试套件 ✅ (102/102 pass) +- [x] CouponFactory.t.sol — 11 tests (含 fuzz) +- [x] Coupon.t.sol — 10 tests +- [x] Settlement.t.sol — 8 tests +- [x] Redemption.t.sol — 6 tests +- [x] Compliance.t.sol — 19 tests +- [x] Treasury.t.sol — 12 tests +- [x] Governance.t.sol — 11 tests +- [x] ExchangeRateOracle.t.sol — 9 tests +- [x] CouponBackedSecurity.t.sol — 10 tests +- [x] Integration.t.sol — 6 tests (端到端) +- **验证**: `forge test` → 102/102 PASS + +### 阶段 6: Genex Chain 核心 (cosmos/evm v0.5.1) ✅ +- [x] app.go — 真实 BaseApp + 20 个模块注册 (auth, bank, staking, evm, feemarket, erc20, ibc...) +- [x] genesis.go — EVM/FeeMarket/Mint 创世状态 +- [x] export.go — 状态导出 + 零高度重置 +- [x] interfaces.go — BankKeeper 接口 +- [x] mempool.go — EVM 内存池配置 +- [x] upgrades.go — 升级处理框架 +- **依赖**: cosmos/evm v0.5.1, Cosmos SDK v0.53.5, CometBFT v0.38.19 + +### 阶段 7: CLI 入口 ✅ +- [x] cmd/genexd/main.go — 入口 + Bech32 配置 (genex prefix) +- [x] cmd/genexd/cmd/root.go — 完整 Cosmos SDK server 命令 + - init, start, keys, genesis, query, tx, status, debug, confix, pruning, snapshot + - cosmos/evm server 命令 (EVM JSON-RPC, WebSocket) + - EthSecp256k1 密钥支持 (MetaMask 兼容) + +### 阶段 8: Gas 补贴模块 ✅ +- [x] x/evm/keeper/gas.go — Gas 价格覆盖、EIP-1559 支持 +- [x] x/evm/keeper/gas_test.go — 13 tests +- **验证**: `go test ./x/evm/keeper/` → 13/13 PASS + +### 阶段 9: 链级合规拦截 ✅ +- [x] x/evm/ante/compliance_ante.go — OFAC + Structuring + Travel Rule +- [x] x/evm/ante/compliance_ante_test.go — 18 tests (含并发安全) +- **验证**: `go test ./x/evm/ante/` → 18/18 PASS + +### 阶段 10: 构建与部署 ✅ +- [x] go.mod — 精确版本钉选 + 4 个 replace 指令 +- [x] Makefile — build/test/docker/init-local/start +- [x] Dockerfile — golang:1.23-alpine 多阶段构建 +- [x] docker-compose.yml — 6节点完整部署 (5验证 + 1监管) +- [x] scripts/init-local.sh — 单节点测试链 +- [x] scripts/init-testnet.sh — 5验证+1监管节点 +- [x] scripts/build-production.sh — 生产构建脚本 + +--- + +## 指南符合性验证 (06-区块链开发指南.md) + +> 逐节检查日期: 2026-02-14 + +### 完全符合 (17/21 节) +§1 链架构, §3 开发环境, §4 智能合约(9>指南7), §5 升级策略, +§7 Gas补贴, §8 测试(133 tests), §9 部署, §11 多机构验证节点, +§12 安全规范, §14 不可转让券, §15 差异化KYC, §17 Treasury, +§18 回滚, §19 多稳定币, §20 Oracle, §21 CBS, §2 设计参数 + +### 已修复偏差 (2项) +1. **§6/§16 合规 ante handler 集成** — 已创建 `compliance_integration.go` 将 + 独立 ComplianceAnteHandler 桥接到 Cosmos SDK ante 链 (OFAC→TravelRule→Structuring→标准handler) +2. **§13 init-testnet.sh denom** — 已修正 `ugnx` → `agnx` (18 decimals) + 更新所有金额 + +### 合理偏差 (不需修复) +- Block-STM 并行执行 (§2): 性能优化特性,后期集成 +- GCFN 三级路由 (§10): 运维架构,非代码层面 +- Axelar 桥 (§1): 外部基础设施集成 +- `agnx` 替代 `ugnx`: EVM 18位精度要求 + +--- + +## 测试验证总结 + +| 组件 | 框架 | 测试数 | 状态 | +|------|------|--------|------| +| 智能合约 (9 contracts) | Foundry | 102 | ✅ ALL PASS | +| 合规模块 (OFAC/Travel Rule) | Go test | 18 | ✅ ALL PASS | +| Gas 模块 (补贴/EIP-1559) | Go test | 13 | ✅ ALL PASS | +| **总计** | | **133** | **✅** | + +--- + +## 构建验证 + +**Windows 环境限制**: 当前开发环境 (Windows, CGO_ENABLED=0) 无法编译 Cosmos SDK。 +链代码的编译和部署通过 Docker 完成: + +```bash +# 方式1: Docker 构建(推荐) +cd blockchain/genex-chain +docker build -t genex-chain:latest . + +# 方式2: Linux 直接构建 +bash scripts/build-production.sh + +# 方式3: 完整测试网部署 +cd blockchain +docker compose up -d +``` + +**已验证组件**: +- ✅ 智能合约: 102/102 Solidity tests pass (Foundry) +- ✅ Go 自定义模块: 31/31 Go tests pass (compliance + gas) +- ⏳ 链二进制编译: 需要 Docker/Linux 环境验证 +- ⏳ 链启动与出块: 需要 Docker 环境验证 +- ⏳ EVM JSON-RPC: 需要 Docker 环境验证 + +--- + +## 合约架构关系 + +``` +Governance (多签治理) + ├── 升级所有合约 + ├── 调整Gas参数 + └── 紧急冻结 + │ +CouponFactory (铸造) + │ 创建 Coupon + ▼ +Coupon (ERC-721) + │ 被 Settlement/Redemption 引用 + ▼ +Settlement (结算) Redemption (兑付) + │ 调用 Compliance │ 销毁 Coupon + │ 使用 Treasury escrow │ + ▼ ▼ +Compliance (合规) Treasury (资金) + │ OFAC + KYC + Travel Rule │ escrow/release/guarantee + ▼ ▼ +ExchangeRateOracle CouponBackedSecurity + (汇率) (CBS证券化) +``` + +--- + +## 安全红线(不可升级逻辑) +1. 券类型标记(Utility/Security)— 铸造后不可修改 +2. 所有权记录 — 不可被升级篡改 +3. 链上转售计数器 — 防止绕过 Utility Track 限制 + +--- + +## 部署说明 + +```bash +# 1. 构建链镜像 +cd blockchain/genex-chain +docker build -t genex-chain:latest . + +# 2. 启动完整测试网(5验证+1监管节点) +cd blockchain +docker compose up -d + +# 3. 仅单节点开发 +docker compose up genex-node-1 -d + +# 4. 部署合约 +docker compose run --profile deploy contract-deployer + +# 5. 检查状态 +docker exec genex-us-east-1 genexd status +curl http://localhost:26657/status # CometBFT RPC +curl http://localhost:8545 # EVM JSON-RPC +``` diff --git a/blockchain/README.md b/blockchain/README.md new file mode 100644 index 0000000..3243873 --- /dev/null +++ b/blockchain/README.md @@ -0,0 +1,131 @@ +# Genex Chain — 区块链模块 + +> 自建 EVM 兼容应用链 + 智能合约体系 + +## 架构 + +``` +blockchain/ +├── genex-chain/ # Cosmos SDK 应用链(Go) +│ ├── cmd/genexd/ # 链节点二进制 +│ ├── app/ # Application 配置 +│ ├── x/evm/ # 自定义 EVM 模块 +│ │ ├── keeper/ # Gas 费覆盖(平台补贴) +│ │ └── ante/ # 链级合规拦截 +│ ├── config/ # 节点/创世配置 +│ └── scripts/ # 初始化脚本 +│ +└── genex-contracts/ # Solidity 智能合约(Foundry) + ├── src/ # 9 个核心合约 + ├── test/ # 10 个测试文件 + └── script/ # 部署脚本 +``` + +## 技术栈 + +| 组件 | 技术 | 说明 | +|------|------|------| +| 链框架 | Cosmos SDK v0.50 | 模块化,200+ 生产链验证 | +| 共识引擎 | CometBFT | 即时终结性,≤1s 出块 | +| EVM 模块 | cosmos/evm | 完全 EVM 兼容,Apache 2.0 | +| 合约框架 | Foundry (Solidity 0.8.20) | 测试 + 部署 + 验证 | +| 跨链 | IBC + Axelar | Cosmos 生态 + Ethereum 桥接 | + +## 智能合约系统(9 合约) + +| 合约 | 功能 | +|------|------| +| CouponFactory | 券发行工厂(Utility/Security 双轨) | +| Coupon | ERC-721 券 NFT(不可转让限制 + 转售计数) | +| Settlement | 原子交换结算(多稳定币 + 价格验证) | +| Redemption | 兑付合约(销毁 + 门店验证) | +| Compliance | 合规(OFAC + Travel Rule + KYC 差异化) | +| Treasury | 资金托管(保障资金 + Escrow) | +| Governance | 治理(3/5 多签 + 48h 时间锁 + 回滚) | +| ExchangeRateOracle | 汇率预言机(Chainlink 集成) | +| CouponBackedSecurity | CBS 资产证券化 | + +## 链设计参数 + +| 参数 | 值 | +|------|-----| +| Chain ID | 8888 | +| 出块时间 | ≤ 1 秒 | +| TPS | ≥ 5,000 | +| Gas 策略 | 平台全额补贴(min_gas_price = 0) | +| 原生代币 | GNX (1B 总供应量) | +| 共识 | CometBFT PoS | +| EVM 兼容 | 完全兼容(Solidity, Hardhat, MetaMask) | + +## 快速开始 + +### 合约开发 + +```bash +cd blockchain/genex-contracts + +# 安装依赖 +forge install OpenZeppelin/openzeppelin-contracts +forge install OpenZeppelin/openzeppelin-contracts-upgradeable + +# 编译 +forge build + +# 测试 +forge test -vvv + +# 部署到本地 +forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast +``` + +### 链开发 + +```bash +cd blockchain/genex-chain + +# 编译 +make build + +# 初始化本地测试链 +make init-local + +# 启动 +make start +``` + +### MetaMask 配置 + +| 设置 | 值 | +|------|-----| +| Network Name | Genex Chain | +| RPC URL | http://localhost:8545 | +| Chain ID | 8888 | +| Currency Symbol | GNX | + +## GNX 代币分配 + +``` +总供应量: 1,000,000,000 GNX +├── 40% 平台运营/Gas 补贴 +├── 20% 团队与顾问(4年释放,1年锁定) +├── 15% 生态基金 +├── 15% 融资预留 +└── 10% 社区 DAO 治理 +``` + +## 验证节点架构 + +``` +生产网络(最少 5 个验证节点): + Genex 创世节点 x3 — US(2) + SG(1) + 机构验证节点 x4+ — 持牌金融机构 + 监管观察节点 x3 — FinCEN / MAS / FCA(只读) +``` + +## 安全规范 + +- 所有合约上线前通过第三方安全审计 +- Transparent Proxy 部署(可升级) +- 升级需 3/5 多签 + 48 小时时间锁 +- 紧急通道: 4/5 多签 + 4 小时时间锁 +- 不可升级的安全红线: 券类型标记、所有权记录、转售计数器 diff --git a/blockchain/docker-compose.yml b/blockchain/docker-compose.yml new file mode 100644 index 0000000..ee87022 --- /dev/null +++ b/blockchain/docker-compose.yml @@ -0,0 +1,191 @@ +# ============================================================ +# Genex Blockchain Stack — Docker Compose +# +# 包含: +# - genex-node-1/2/3: 3个创世验证节点 +# - genex-inst-1/2: 2个机构验证节点 +# - genex-regulatory: 1个监管只读节点 +# - contract-deployer: 智能合约部署任务 +# +# 启动: +# docker compose up -d +# +# 仅启动单节点开发模式: +# docker compose up genex-node-1 -d +# +# 部署合约: +# docker compose run contract-deployer +# ============================================================ + +version: "3.9" + +x-genex-node: &genex-node-defaults + build: + context: ./genex-chain + dockerfile: Dockerfile + restart: unless-stopped + networks: + - genex-net + +services: + # ============================================= + # Genesis Validator Node 1 (Primary, US-East) + # ============================================= + genex-node-1: + <<: *genex-node-defaults + container_name: genex-us-east-1 + hostname: genex-us-east-1 + environment: + - MONIKER=genex-us-east-1 + - CHAIN_ID=genex-testnet-1 + - NODE_TYPE=genesis + ports: + - "26656:26656" # P2P + - "26657:26657" # CometBFT RPC + - "8545:8545" # EVM JSON-RPC + - "8546:8546" # EVM WebSocket + - "1317:1317" # Cosmos REST API + - "9090:9090" # gRPC + volumes: + - node1-data:/home/genex/.genexd + + # ============================================= + # Genesis Validator Node 2 (US-West) + # ============================================= + genex-node-2: + <<: *genex-node-defaults + container_name: genex-us-west-1 + hostname: genex-us-west-1 + environment: + - MONIKER=genex-us-west-1 + - CHAIN_ID=genex-testnet-1 + - NODE_TYPE=genesis + - PERSISTENT_PEERS=genex-us-east-1:26656 + ports: + - "26666:26656" + - "26667:26657" + - "8555:8545" + volumes: + - node2-data:/home/genex/.genexd + depends_on: + - genex-node-1 + + # ============================================= + # Genesis Validator Node 3 (Singapore) + # ============================================= + genex-node-3: + <<: *genex-node-defaults + container_name: genex-sg-1 + hostname: genex-sg-1 + environment: + - MONIKER=genex-sg-1 + - CHAIN_ID=genex-testnet-1 + - NODE_TYPE=genesis + - PERSISTENT_PEERS=genex-us-east-1:26656,genex-us-west-1:26656 + ports: + - "26676:26656" + - "26677:26657" + - "8565:8545" + volumes: + - node3-data:/home/genex/.genexd + depends_on: + - genex-node-1 + + # ============================================= + # Institution Validator Node 1 + # ============================================= + genex-inst-1: + <<: *genex-node-defaults + container_name: genex-inst-1 + hostname: genex-inst-1 + environment: + - MONIKER=genex-inst-1 + - CHAIN_ID=genex-testnet-1 + - NODE_TYPE=institution + - PERSISTENT_PEERS=genex-us-east-1:26656,genex-sg-1:26656 + ports: + - "26686:26656" + - "26687:26657" + - "8575:8545" + volumes: + - inst1-data:/home/genex/.genexd + depends_on: + - genex-node-1 + - genex-node-3 + + # ============================================= + # Institution Validator Node 2 + # ============================================= + genex-inst-2: + <<: *genex-node-defaults + container_name: genex-inst-2 + hostname: genex-inst-2 + environment: + - MONIKER=genex-inst-2 + - CHAIN_ID=genex-testnet-1 + - NODE_TYPE=institution + - PERSISTENT_PEERS=genex-us-east-1:26656,genex-inst-1:26656 + ports: + - "26696:26656" + - "26697:26657" + - "8585:8545" + volumes: + - inst2-data:/home/genex/.genexd + depends_on: + - genex-node-1 + + # ============================================= + # Regulatory Observer Node (Read-Only) + # ============================================= + genex-regulatory: + <<: *genex-node-defaults + container_name: genex-regulatory-1 + hostname: genex-regulatory-1 + environment: + - MONIKER=genex-regulatory-1 + - CHAIN_ID=genex-testnet-1 + - NODE_TYPE=regulatory + - PERSISTENT_PEERS=genex-us-east-1:26656,genex-sg-1:26656 + ports: + - "26706:26656" + - "26707:26657" + - "8595:8545" + volumes: + - regulatory-data:/home/genex/.genexd + depends_on: + - genex-node-1 + + # ============================================= + # Smart Contract Deployer (one-shot job) + # ============================================= + contract-deployer: + build: + context: ./genex-contracts + dockerfile: Dockerfile + container_name: genex-contract-deployer + environment: + - RPC_URL=http://genex-node-1:8545 + - DEPLOYER_PRIVATE_KEY=${DEPLOYER_PRIVATE_KEY:-} + - USDC_ADDRESS=${USDC_ADDRESS:-} + - FEE_COLLECTOR=${FEE_COLLECTOR:-} + networks: + - genex-net + depends_on: + - genex-node-1 + profiles: + - deploy + +volumes: + node1-data: + node2-data: + node3-data: + inst1-data: + inst2-data: + regulatory-data: + +networks: + genex-net: + driver: bridge + ipam: + config: + - subnet: 172.28.0.0/16 diff --git a/blockchain/genex-chain/.dockerignore b/blockchain/genex-chain/.dockerignore new file mode 100644 index 0000000..fa1780d --- /dev/null +++ b/blockchain/genex-chain/.dockerignore @@ -0,0 +1,9 @@ +build/ +testnet/ +.scaffold/ +coverage.out +coverage.html +*.log +.git +.gitignore +*.md diff --git a/blockchain/genex-chain/Dockerfile b/blockchain/genex-chain/Dockerfile new file mode 100644 index 0000000..4cbcae5 --- /dev/null +++ b/blockchain/genex-chain/Dockerfile @@ -0,0 +1,90 @@ +# ============================================================ +# Genex Chain — Multi-stage Docker Build +# ============================================================ +# 基于 cosmos/evm v0.5.1, Cosmos SDK v0.53.5, CometBFT v0.38.19 +# +# 用法: +# docker build -t genex-chain:latest . +# docker run -p 26657:26657 -p 8545:8545 genex-chain:latest +# ============================================================ + +# ======================== +# Stage 1: Builder +# ======================== +FROM golang:1.23-alpine AS builder + +RUN apk add --no-cache \ + make \ + gcc \ + musl-dev \ + linux-headers \ + git \ + bash \ + curl + +WORKDIR /build + +# Copy go.mod first for dependency caching +COPY go.mod ./ +RUN go mod download 2>/dev/null || true + +# Copy source code +COPY . . + +# Resolve all dependencies +RUN go mod tidy + +# Build the binary with CGO (required for Cosmos SDK crypto) +RUN CGO_ENABLED=1 GOOS=linux go build \ + -ldflags "-X github.com/cosmos/cosmos-sdk/version.Name=GenexChain \ + -X github.com/cosmos/cosmos-sdk/version.AppName=genexd \ + -X github.com/cosmos/cosmos-sdk/version.Version=1.0.0 \ + -X github.com/cosmos/cosmos-sdk/version.Commit=$(git rev-parse --short HEAD 2>/dev/null || echo 'dev') \ + -X github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger \ + -w -s" \ + -tags "netgo,ledger" \ + -trimpath \ + -o /build/bin/genexd \ + ./cmd/genexd/ + +# ======================== +# Stage 2: Runtime +# ======================== +FROM alpine:3.19 + +RUN apk add --no-cache \ + ca-certificates \ + bash \ + curl \ + jq + +# Create non-root user +RUN addgroup -S genex && adduser -S genex -G genex + +# Copy binary from builder +COPY --from=builder /build/bin/genexd /usr/local/bin/genexd + +# Copy initialization scripts +COPY scripts/ /opt/genex/scripts/ + +RUN chmod +x /usr/local/bin/genexd && \ + chmod +x /opt/genex/scripts/*.sh 2>/dev/null || true + +# Set up data directory +RUN mkdir -p /home/genex/.genexd && \ + chown -R genex:genex /home/genex + +USER genex +WORKDIR /home/genex + +# Expose ports +# P2P: 26656, RPC: 26657, EVM JSON-RPC: 8545, EVM WS: 8546 +# REST API: 1317, gRPC: 9090, gRPC-web: 9091 +EXPOSE 26656 26657 8545 8546 1317 9090 9091 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD curl -f http://localhost:26657/status || exit 1 + +ENTRYPOINT ["genexd"] +CMD ["start"] diff --git a/blockchain/genex-chain/Makefile b/blockchain/genex-chain/Makefile new file mode 100644 index 0000000..1cb390a --- /dev/null +++ b/blockchain/genex-chain/Makefile @@ -0,0 +1,127 @@ +# ============================================================ +# Genex Chain — Makefile +# ============================================================ +# 基于 Cosmos SDK + cosmos/evm + CometBFT +# +# 需要: Go 1.23.8+, CGO, Linux/macOS (或 Docker) +# ============================================================ + +BINARY_NAME := genexd +APP_NAME := GenexChain +VERSION := 1.0.0 +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "dev") +CHAIN_ID := genex-1 +EVM_CHAIN_ID := 8888 +BUILD_DIR := build +DOCKER_IMAGE := genex-chain + +# Build flags +LDFLAGS := -X github.com/cosmos/cosmos-sdk/version.Name=$(APP_NAME) \ + -X github.com/cosmos/cosmos-sdk/version.AppName=$(BINARY_NAME) \ + -X github.com/cosmos/cosmos-sdk/version.Version=$(VERSION) \ + -X github.com/cosmos/cosmos-sdk/version.Commit=$(COMMIT) \ + -X github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger \ + -w -s + +BUILD_FLAGS := -ldflags '$(LDFLAGS)' -tags "netgo,ledger" -trimpath + +.PHONY: all build install clean test lint docker docker-run docker-stop \ + init-local start deps format help version + +## help: Display this help message +help: + @echo "Genex Chain — Build System" + @echo "" + @echo "Usage: make " + @echo "" + @echo "Targets:" + @grep -E '^## ' Makefile | sed 's/## / /' + +## all: Build the binary +all: build + +## deps: Download and tidy dependencies +deps: + @echo "==> Downloading dependencies..." + go mod tidy + go mod download + +## build: Build genexd binary +build: deps + @echo "==> Building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=1 go build $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/genexd/ + @echo "Binary: $(BUILD_DIR)/$(BINARY_NAME)" + +## install: Install genexd to GOPATH/bin +install: deps + @echo "==> Installing $(BINARY_NAME)..." + CGO_ENABLED=1 go install $(BUILD_FLAGS) ./cmd/genexd/ + +## test: Run all tests +test: + @echo "==> Running tests..." + go test -v ./x/evm/ante/ ./x/evm/keeper/ + @echo "All tests passed!" + +## test-contracts: Run Solidity contract tests (requires Foundry) +test-contracts: + @echo "==> Running contract tests..." + cd ../genex-contracts && forge test -vvv + +## lint: Run linters +lint: + @echo "==> Running linters..." + @which golangci-lint > /dev/null 2>&1 || (echo "golangci-lint not found" && exit 1) + golangci-lint run ./... + +## format: Format Go code +format: + @echo "==> Formatting code..." + gofmt -s -w . + +## clean: Remove build artifacts +clean: + @echo "==> Cleaning..." + rm -rf $(BUILD_DIR) + +## init-local: Initialize a local single-node testnet +init-local: build + @echo "==> Initializing local testnet..." + $(BUILD_DIR)/$(BINARY_NAME) init genex-local --chain-id $(CHAIN_ID) + @echo "Local testnet initialized at ~/.genexd" + @echo "Start with: make start" + +## start: Start the local node +start: + @echo "==> Starting Genex Chain..." + $(BUILD_DIR)/$(BINARY_NAME) start \ + --minimum-gas-prices 0agnx \ + --evm.chain-id $(EVM_CHAIN_ID) + +## docker: Build Docker image +docker: + @echo "==> Building Docker image..." + docker build -t $(DOCKER_IMAGE):$(VERSION) -t $(DOCKER_IMAGE):latest . + @echo "Image: $(DOCKER_IMAGE):$(VERSION)" + +## docker-run: Run single node in Docker +docker-run: + @echo "==> Running Genex Chain in Docker..." + docker run -d --name genex-node \ + -p 26656:26656 -p 26657:26657 \ + -p 8545:8545 -p 8546:8546 \ + -p 1317:1317 -p 9090:9090 \ + $(DOCKER_IMAGE):latest + +## docker-stop: Stop Docker node +docker-stop: + docker stop genex-node && docker rm genex-node + +## version: Show version +version: + @echo "$(APP_NAME) $(VERSION) ($(COMMIT))" + @echo "Chain ID: $(CHAIN_ID)" + @echo "EVM Chain ID: $(EVM_CHAIN_ID)" + @echo "Denom: agnx (GNX)" + @echo "Bech32: genex" diff --git a/blockchain/genex-chain/app.go b/blockchain/genex-chain/app.go new file mode 100644 index 0000000..7d70532 --- /dev/null +++ b/blockchain/genex-chain/app.go @@ -0,0 +1,978 @@ +// Package genexchain — Genex Chain Application +// +// Genex Chain 是基于 Cosmos SDK + cosmos/evm + CometBFT 构建的券金融专用应用链。 +// 基于 cosmos/evm v0.5.1 的 evmd 示例链定制。 +// +// 特性: +// - 完全 EVM 兼容 (Solidity, Hardhat, MetaMask) +// - CometBFT 即时终结性共识 (≤1s 出块) +// - 链级合规 (OFAC, Travel Rule, KYC) +// - 平台 Gas 全额补贴 (用户零 Gas) +// - IBC 跨链 + ERC-20 代币桥接 +package genexchain + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + + "github.com/spf13/cast" + + // Force-load the tracer engines to trigger registration + "github.com/ethereum/go-ethereum/common" + _ "github.com/ethereum/go-ethereum/eth/tracers/js" + _ "github.com/ethereum/go-ethereum/eth/tracers/native" + + abci "github.com/cometbft/cometbft/abci/types" + + dbm "github.com/cosmos/cosmos-db" + evmante "github.com/cosmos/evm/ante" + antetypes "github.com/cosmos/evm/ante/types" + evmconfig "github.com/cosmos/evm/config" + evmencoding "github.com/cosmos/evm/encoding" + evmaddress "github.com/cosmos/evm/encoding/address" + evmmempool "github.com/cosmos/evm/mempool" + precompiletypes "github.com/cosmos/evm/precompiles/types" + srvflags "github.com/cosmos/evm/server/flags" + "github.com/cosmos/evm/utils" + "github.com/cosmos/evm/x/erc20" + erc20keeper "github.com/cosmos/evm/x/erc20/keeper" + erc20types "github.com/cosmos/evm/x/erc20/types" + erc20v2 "github.com/cosmos/evm/x/erc20/v2" + "github.com/cosmos/evm/x/feemarket" + feemarketkeeper "github.com/cosmos/evm/x/feemarket/keeper" + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" + ibccallbackskeeper "github.com/cosmos/evm/x/ibc/callbacks/keeper" + "github.com/cosmos/evm/x/ibc/transfer" + transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper" + transferv2 "github.com/cosmos/evm/x/ibc/transfer/v2" + "github.com/cosmos/evm/x/precisebank" + precisebankkeeper "github.com/cosmos/evm/x/precisebank/keeper" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" + "github.com/cosmos/evm/x/vm" + evmkeeper "github.com/cosmos/evm/x/vm/keeper" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/cosmos/gogoproto/proto" + ibccallbacks "github.com/cosmos/ibc-go/v10/modules/apps/callbacks" + ibctransfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" + ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + ibc "github.com/cosmos/ibc-go/v10/modules/core" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcapi "github.com/cosmos/ibc-go/v10/modules/core/api" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" + ibckeeper "github.com/cosmos/ibc-go/v10/modules/core/keeper" + ibctm "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" + ibctesting "github.com/cosmos/ibc-go/v10/testing" + + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" + reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" + "cosmossdk.io/client/v2/autocli" + "cosmossdk.io/core/appmodule" + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + "cosmossdk.io/x/evidence" + evidencekeeper "cosmossdk.io/x/evidence/keeper" + evidencetypes "cosmossdk.io/x/evidence/types" + "cosmossdk.io/x/feegrant" + feegrantkeeper "cosmossdk.io/x/feegrant/keeper" + feegrantmodule "cosmossdk.io/x/feegrant/module" + "cosmossdk.io/x/upgrade" + upgradekeeper "cosmossdk.io/x/upgrade/keeper" + upgradetypes "cosmossdk.io/x/upgrade/types" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" + "github.com/cosmos/cosmos-sdk/client/grpc/node" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/runtime" + runtimeservices "github.com/cosmos/cosmos-sdk/runtime/services" + sdkserver "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/api" + "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + testdata_pulsar "github.com/cosmos/cosmos-sdk/testutil/testdata/testpb" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/types/msgservice" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + "github.com/cosmos/cosmos-sdk/x/auth/posthandler" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + txmodule "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + authzmodule "github.com/cosmos/cosmos-sdk/x/authz/module" + "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/consensus" + consensusparamkeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" + consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/mint" + mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + cosmosevmserver "github.com/cosmos/evm/server" + + genexante "github.com/genex/genex-chain/x/evm/ante" +) + +const ( + AppName = "GenexChain" + + // Genex Chain 参数 + GenexBech32Prefix = "genex" + GenexBondDenom = "agnx" // atto GNX, 18 decimals for EVM compatibility + GenexDisplayDenom = "GNX" + GenexEVMChainID = uint64(8888) +) + +var defaultNodeHome string + +func init() { + // Use atto power reduction (18 decimals) for EVM compatibility + sdk.DefaultPowerReduction = utils.AttoPowerReduction + defaultNodeHome = GenexDefaultNodeHome() +} + +// GenexDefaultNodeHome returns the default node home directory (~/.genexd) +func GenexDefaultNodeHome() string { + homeDir, _ := os.UserHomeDir() + return homeDir + "/.genexd" +} + +// SetGenexBech32Prefixes sets the bech32 prefixes for Genex Chain +func SetGenexBech32Prefixes(config *sdk.Config) { + config.SetBech32PrefixForAccount(GenexBech32Prefix, GenexBech32Prefix+"pub") + config.SetBech32PrefixForValidator(GenexBech32Prefix+"valoper", GenexBech32Prefix+"valoperpub") + config.SetBech32PrefixForConsensusNode(GenexBech32Prefix+"valcons", GenexBech32Prefix+"valconspub") +} + +var ( + _ runtime.AppI = (*GenexApp)(nil) + _ cosmosevmserver.Application = (*GenexApp)(nil) + _ ibctesting.TestingApp = (*GenexApp)(nil) +) + +// GenexApp 是 Genex Chain 的核心 Application +// 继承 Cosmos SDK BaseApp,集成 cosmos/evm EVM 模块 +type GenexApp struct { + *baseapp.BaseApp + + legacyAmino *codec.LegacyAmino + appCodec codec.Codec + interfaceRegistry types.InterfaceRegistry + txConfig client.TxConfig + clientCtx client.Context + + pendingTxListeners []evmante.PendingTxListener + + // keys to access the substores + keys map[string]*storetypes.KVStoreKey + tkeys map[string]*storetypes.TransientStoreKey + memKeys map[string]*storetypes.MemoryStoreKey + + // Cosmos SDK keepers + AccountKeeper authkeeper.AccountKeeper + BankKeeper bankkeeper.Keeper + StakingKeeper *stakingkeeper.Keeper + SlashingKeeper slashingkeeper.Keeper + MintKeeper mintkeeper.Keeper + DistrKeeper distrkeeper.Keeper + GovKeeper govkeeper.Keeper + UpgradeKeeper *upgradekeeper.Keeper + AuthzKeeper authzkeeper.Keeper + EvidenceKeeper evidencekeeper.Keeper + FeeGrantKeeper feegrantkeeper.Keeper + ConsensusParamsKeeper consensusparamkeeper.Keeper + + // IBC keepers + IBCKeeper *ibckeeper.Keeper + TransferKeeper transferkeeper.Keeper + CallbackKeeper ibccallbackskeeper.ContractKeeper + + // Cosmos EVM keepers + FeeMarketKeeper feemarketkeeper.Keeper + EVMKeeper *evmkeeper.Keeper + Erc20Keeper erc20keeper.Keeper + PreciseBankKeeper precisebankkeeper.Keeper + EVMMempool *evmmempool.ExperimentalEVMMempool + + // Genex compliance handler — 验证节点级合规拦截 (OFAC, Travel Rule, Structuring) + // 链下合规服务通过此 handler 同步 OFAC 名单和 Travel Rule 记录 + ComplianceHandler *genexante.ComplianceAnteHandler + + // Module manager + ModuleManager *module.Manager + BasicModuleManager module.BasicManager + + // Simulation manager + sm *module.SimulationManager + + // Module configurator + configurator module.Configurator +} + +// NewGenexApp 创建 GenexApp 实例 +func NewGenexApp( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + loadLatest bool, + appOpts servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), +) *GenexApp { + evmChainID := cast.ToUint64(appOpts.Get(srvflags.EVMChainID)) + if evmChainID == 0 { + evmChainID = GenexEVMChainID + } + encodingConfig := evmencoding.MakeConfig(evmChainID) + + appCodec := encodingConfig.Codec + legacyAmino := encodingConfig.Amino + interfaceRegistry := encodingConfig.InterfaceRegistry + txConfig := encodingConfig.TxConfig + + bApp := baseapp.NewBaseApp( + AppName, logger, db, + encodingConfig.TxConfig.TxDecoder(), + baseAppOptions..., + ) + bApp.SetCommitMultiStoreTracer(traceStore) + bApp.SetVersion(version.Version) + bApp.SetInterfaceRegistry(interfaceRegistry) + bApp.SetTxEncoder(txConfig.TxEncoder()) + + keys := storetypes.NewKVStoreKeys( + // Cosmos SDK + authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, + minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, + govtypes.StoreKey, consensusparamtypes.StoreKey, + upgradetypes.StoreKey, feegrant.StoreKey, evidencetypes.StoreKey, authzkeeper.StoreKey, + // IBC + ibcexported.StoreKey, ibctransfertypes.StoreKey, + // Cosmos EVM + evmtypes.StoreKey, feemarkettypes.StoreKey, erc20types.StoreKey, precisebanktypes.StoreKey, + ) + + tkeys := storetypes.NewTransientStoreKeys(evmtypes.TransientKey, feemarkettypes.TransientKey) + + if err := bApp.RegisterStreamingServices(appOpts, keys); err != nil { + fmt.Printf("failed to load state streaming: %s", err) + os.Exit(1) + } + + app := &GenexApp{ + BaseApp: bApp, + legacyAmino: legacyAmino, + appCodec: appCodec, + txConfig: txConfig, + interfaceRegistry: interfaceRegistry, + keys: keys, + tkeys: tkeys, + } + + // Authority address (governance module) + authAddr := authtypes.NewModuleAddress(govtypes.ModuleName).String() + + // Consensus params keeper + app.ConsensusParamsKeeper = consensusparamkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[consensusparamtypes.StoreKey]), + authAddr, + runtime.EventService{}, + ) + bApp.SetParamStore(app.ConsensusParamsKeeper.ParamsStore) + + // Account keeper + app.AccountKeeper = authkeeper.NewAccountKeeper( + appCodec, runtime.NewKVStoreService(keys[authtypes.StoreKey]), + authtypes.ProtoBaseAccount, evmconfig.GetMaccPerms(), + evmaddress.NewEvmCodec(GenexBech32Prefix), + GenexBech32Prefix, + authAddr, + ) + + // Bank keeper + app.BankKeeper = bankkeeper.NewBaseKeeper( + appCodec, + runtime.NewKVStoreService(keys[banktypes.StoreKey]), + app.AccountKeeper, + evmconfig.BlockedAddresses(), + authAddr, + logger, + ) + + // Enable SIGN_MODE_TEXTUAL + enabledSignModes := append(authtx.DefaultSignModes, signingtypes.SignMode_SIGN_MODE_TEXTUAL) + txConfigOpts := authtx.ConfigOptions{ + EnabledSignModes: enabledSignModes, + TextualCoinMetadataQueryFn: txmodule.NewBankKeeperCoinMetadataQueryFn(app.BankKeeper), + } + txConfig, err := authtx.NewTxConfigWithOptions(appCodec, txConfigOpts) + if err != nil { + panic(err) + } + app.txConfig = txConfig + + // Staking keeper + app.StakingKeeper = stakingkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[stakingtypes.StoreKey]), + app.AccountKeeper, + app.BankKeeper, + authAddr, + evmaddress.NewEvmCodec(GenexBech32Prefix+"valoper"), + evmaddress.NewEvmCodec(GenexBech32Prefix+"valcons"), + ) + + // Mint keeper + app.MintKeeper = mintkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[minttypes.StoreKey]), + app.StakingKeeper, + app.AccountKeeper, + app.BankKeeper, + authtypes.FeeCollectorName, + authAddr, + ) + + // Distribution keeper + app.DistrKeeper = distrkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[distrtypes.StoreKey]), + app.AccountKeeper, + app.BankKeeper, + app.StakingKeeper, + authtypes.FeeCollectorName, + authAddr, + ) + + // Slashing keeper + app.SlashingKeeper = slashingkeeper.NewKeeper( + appCodec, + app.LegacyAmino(), + runtime.NewKVStoreService(keys[slashingtypes.StoreKey]), + app.StakingKeeper, + authAddr, + ) + + // FeeGrant keeper + app.FeeGrantKeeper = feegrantkeeper.NewKeeper( + appCodec, runtime.NewKVStoreService(keys[feegrant.StoreKey]), app.AccountKeeper, + ) + + // Staking hooks + app.StakingKeeper.SetHooks( + stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.SlashingKeeper.Hooks()), + ) + + // Authz keeper + app.AuthzKeeper = authzkeeper.NewKeeper( + runtime.NewKVStoreService(keys[authzkeeper.StoreKey]), + appCodec, + app.MsgServiceRouter(), + app.AccountKeeper, + ) + + // Upgrade keeper + skipUpgradeHeights := map[int64]bool{} + for _, h := range cast.ToIntSlice(appOpts.Get(sdkserver.FlagUnsafeSkipUpgrades)) { + skipUpgradeHeights[int64(h)] = true + } + homePath := cast.ToString(appOpts.Get(flags.FlagHome)) + app.UpgradeKeeper = upgradekeeper.NewKeeper( + skipUpgradeHeights, + runtime.NewKVStoreService(keys[upgradetypes.StoreKey]), + appCodec, + homePath, + app.BaseApp, + authAddr, + ) + + // IBC keeper + app.IBCKeeper = ibckeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[ibcexported.StoreKey]), + nil, + app.UpgradeKeeper, + authAddr, + ) + + // Gov keeper + govConfig := govtypes.DefaultConfig() + govKeeper := govkeeper.NewKeeper( + appCodec, runtime.NewKVStoreService(keys[govtypes.StoreKey]), + app.AccountKeeper, app.BankKeeper, + app.StakingKeeper, app.DistrKeeper, + app.MsgServiceRouter(), govConfig, authAddr, + ) + app.GovKeeper = *govKeeper.SetHooks(govtypes.NewMultiGovHooks()) + + // Evidence keeper + evidenceKeeper := evidencekeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[evidencetypes.StoreKey]), + app.StakingKeeper, + app.SlashingKeeper, + app.AccountKeeper.AddressCodec(), + runtime.ProvideCometInfoService(), + ) + app.EvidenceKeeper = *evidenceKeeper + + // ============================================================ + // Cosmos EVM keepers + // ============================================================ + + // FeeMarket keeper + app.FeeMarketKeeper = feemarketkeeper.NewKeeper( + appCodec, authtypes.NewModuleAddress(govtypes.ModuleName), + keys[feemarkettypes.StoreKey], + tkeys[feemarkettypes.TransientKey], + ) + + // PreciseBank keeper (handles EVM 18-decimal conversion) + app.PreciseBankKeeper = precisebankkeeper.NewKeeper( + appCodec, + keys[precisebanktypes.StoreKey], + app.BankKeeper, + app.AccountKeeper, + ) + + // EVM keeper + tracer := cast.ToString(appOpts.Get(srvflags.EVMTracer)) + app.EVMKeeper = evmkeeper.NewKeeper( + appCodec, keys[evmtypes.StoreKey], tkeys[evmtypes.TransientKey], keys, + authtypes.NewModuleAddress(govtypes.ModuleName), + app.AccountKeeper, + app.PreciseBankKeeper, + app.StakingKeeper, + app.FeeMarketKeeper, + &app.ConsensusParamsKeeper, + &app.Erc20Keeper, + evmChainID, + tracer, + ).WithStaticPrecompiles( + precompiletypes.DefaultStaticPrecompiles( + *app.StakingKeeper, + app.DistrKeeper, + app.PreciseBankKeeper, + &app.Erc20Keeper, + &app.TransferKeeper, + app.IBCKeeper.ChannelKeeper, + app.GovKeeper, + app.SlashingKeeper, + appCodec, + ), + ) + + // ERC-20 keeper + app.Erc20Keeper = erc20keeper.NewKeeper( + keys[erc20types.StoreKey], + appCodec, + authtypes.NewModuleAddress(govtypes.ModuleName), + app.AccountKeeper, + app.PreciseBankKeeper, + app.EVMKeeper, + app.StakingKeeper, + &app.TransferKeeper, + ) + + // IBC Transfer keeper (after ERC-20 keeper) + app.TransferKeeper = transferkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]), + app.IBCKeeper.ChannelKeeper, + app.IBCKeeper.ChannelKeeper, + app.MsgServiceRouter(), + app.AccountKeeper, + app.BankKeeper, + app.Erc20Keeper, + authAddr, + ) + app.TransferKeeper.SetAddressCodec(evmaddress.NewEvmCodec(GenexBech32Prefix)) + + // ============================================================ + // IBC Transfer Stack (bottom to top): + // IBC Transfer → ERC-20 Middleware → IBC Callbacks + // ============================================================ + var transferStack porttypes.IBCModule + transferStack = transfer.NewIBCModule(app.TransferKeeper) + maxCallbackGas := uint64(1_000_000) + transferStack = erc20.NewIBCMiddleware(app.Erc20Keeper, transferStack) + app.CallbackKeeper = ibccallbackskeeper.NewKeeper( + app.AccountKeeper, app.EVMKeeper, app.Erc20Keeper, + ) + transferStack = ibccallbacks.NewIBCMiddleware( + transferStack, app.IBCKeeper.ChannelKeeper, app.CallbackKeeper, maxCallbackGas, + ) + + var transferStackV2 ibcapi.IBCModule + transferStackV2 = transferv2.NewIBCModule(app.TransferKeeper) + transferStackV2 = erc20v2.NewIBCMiddleware(transferStackV2, app.Erc20Keeper) + + // IBC Router + ibcRouter := porttypes.NewRouter() + ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) + ibcRouterV2 := ibcapi.NewRouter() + ibcRouterV2.AddRoute(ibctransfertypes.ModuleName, transferStackV2) + + app.IBCKeeper.SetRouter(ibcRouter) + app.IBCKeeper.SetRouterV2(ibcRouterV2) + + // Light client + clientKeeper := app.IBCKeeper.ClientKeeper + storeProvider := app.IBCKeeper.ClientKeeper.GetStoreProvider() + tmLightClientModule := ibctm.NewLightClientModule(appCodec, storeProvider) + clientKeeper.AddRoute(ibctm.ModuleName, &tmLightClientModule) + + transferModule := transfer.NewAppModule(app.TransferKeeper) + + // ============================================================ + // Module Manager + // ============================================================ + app.ModuleManager = module.NewManager( + // Cosmos SDK modules + genutil.NewAppModule(app.AccountKeeper, app.StakingKeeper, app, app.txConfig), + auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, nil), + bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper, nil), + feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), + gov.NewAppModule(appCodec, &app.GovKeeper, app.AccountKeeper, app.BankKeeper, nil), + mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper, nil, nil), + slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper, nil, app.interfaceRegistry), + distr.NewAppModule(appCodec, app.DistrKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper, nil), + staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, nil), + upgrade.NewAppModule(app.UpgradeKeeper, app.AccountKeeper.AddressCodec()), + evidence.NewAppModule(app.EvidenceKeeper), + authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), + consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper), + vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), + // IBC modules + ibc.NewAppModule(app.IBCKeeper), + ibctm.NewAppModule(tmLightClientModule), + transferModule, + // Cosmos EVM modules + vm.NewAppModule(app.EVMKeeper, app.AccountKeeper, app.BankKeeper, app.AccountKeeper.AddressCodec()), + feemarket.NewAppModule(app.FeeMarketKeeper), + erc20.NewAppModule(app.Erc20Keeper, app.AccountKeeper), + precisebank.NewAppModule(app.PreciseBankKeeper, app.BankKeeper, app.AccountKeeper), + ) + + // Basic module manager + app.BasicModuleManager = module.NewBasicManagerFromManager( + app.ModuleManager, + map[string]module.AppModuleBasic{ + genutiltypes.ModuleName: genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), + stakingtypes.ModuleName: staking.AppModuleBasic{}, + govtypes.ModuleName: gov.NewAppModuleBasic(nil), + ibctransfertypes.ModuleName: transfer.AppModuleBasic{AppModuleBasic: &ibctransfer.AppModuleBasic{}}, + }, + ) + app.BasicModuleManager.RegisterLegacyAminoCodec(legacyAmino) + app.BasicModuleManager.RegisterInterfaces(interfaceRegistry) + + // Module ordering + app.ModuleManager.SetOrderPreBlockers( + upgradetypes.ModuleName, + authtypes.ModuleName, + evmtypes.ModuleName, + ) + + app.ModuleManager.SetOrderBeginBlockers( + minttypes.ModuleName, + ibcexported.ModuleName, ibctransfertypes.ModuleName, + erc20types.ModuleName, feemarkettypes.ModuleName, + evmtypes.ModuleName, + distrtypes.ModuleName, slashingtypes.ModuleName, + evidencetypes.ModuleName, stakingtypes.ModuleName, + authtypes.ModuleName, banktypes.ModuleName, govtypes.ModuleName, genutiltypes.ModuleName, + authz.ModuleName, feegrant.ModuleName, + consensusparamtypes.ModuleName, + precisebanktypes.ModuleName, + vestingtypes.ModuleName, + ) + + app.ModuleManager.SetOrderEndBlockers( + govtypes.ModuleName, stakingtypes.ModuleName, + authtypes.ModuleName, banktypes.ModuleName, + evmtypes.ModuleName, erc20types.ModuleName, feemarkettypes.ModuleName, + ibcexported.ModuleName, ibctransfertypes.ModuleName, + distrtypes.ModuleName, + slashingtypes.ModuleName, minttypes.ModuleName, + genutiltypes.ModuleName, evidencetypes.ModuleName, authz.ModuleName, + feegrant.ModuleName, upgradetypes.ModuleName, consensusparamtypes.ModuleName, + precisebanktypes.ModuleName, + vestingtypes.ModuleName, + ) + + genesisModuleOrder := []string{ + authtypes.ModuleName, banktypes.ModuleName, + distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, + minttypes.ModuleName, + ibcexported.ModuleName, + evmtypes.ModuleName, + feemarkettypes.ModuleName, + erc20types.ModuleName, + precisebanktypes.ModuleName, + ibctransfertypes.ModuleName, + genutiltypes.ModuleName, evidencetypes.ModuleName, authz.ModuleName, + feegrant.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, + } + app.ModuleManager.SetOrderInitGenesis(genesisModuleOrder...) + app.ModuleManager.SetOrderExportGenesis(genesisModuleOrder...) + + app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) + if err = app.ModuleManager.RegisterServices(app.configurator); err != nil { + panic(fmt.Sprintf("failed to register services: %s", err.Error())) + } + + app.RegisterUpgradeHandlers() + + autocliv1.RegisterQueryServer(app.GRPCQueryRouter(), runtimeservices.NewAutoCLIQueryService(app.ModuleManager.Modules)) + reflectionSvc, err := runtimeservices.NewReflectionService() + if err != nil { + panic(err) + } + reflectionv1.RegisterReflectionServiceServer(app.GRPCQueryRouter(), reflectionSvc) + testdata_pulsar.RegisterQueryServer(app.GRPCQueryRouter(), testdata_pulsar.QueryImpl{}) + + // Simulation manager + overrideModules := map[string]module.AppModuleSimulation{ + authtypes.ModuleName: auth.NewAppModule(app.appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, nil), + } + app.sm = module.NewSimulationManagerFromAppModules(app.ModuleManager.Modules, overrideModules) + app.sm.RegisterStoreDecoders() + + // Mount stores + app.MountKVStores(keys) + app.MountTransientStores(tkeys) + + maxGasWanted := cast.ToUint64(appOpts.Get(srvflags.EVMMaxTxGasWanted)) + + // Set lifecycle handlers + app.SetInitChainer(app.InitChainer) + app.SetPreBlocker(app.PreBlocker) + app.SetBeginBlocker(app.BeginBlocker) + app.SetEndBlocker(app.EndBlocker) + + app.setAnteHandler(app.txConfig, maxGasWanted) + + // EVM mempool + if err := app.configureEVMMempool(appOpts, logger); err != nil { + panic(fmt.Sprintf("failed to configure EVM mempool: %s", err.Error())) + } + + app.setPostHandler() + + // Validate proto annotations + protoFiles, err := proto.MergedRegistry() + if err != nil { + panic(err) + } + err = msgservice.ValidateProtoAnnotations(protoFiles) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + } + + if loadLatest { + if err := app.LoadLatestVersion(); err != nil { + logger.Error("error on loading last version", "err", err) + os.Exit(1) + } + } + + return app +} + +// setAnteHandler configures the ante handler chain including Genex compliance checks. +// +// Ante handler 链结构: +// +// TX → Genex ComplianceAnteWrapper (OFAC/TravelRule/Structuring 拦截) +// → cosmos/evm AnteHandler (签名验证/Gas计算/Nonce检查/EVM路由) +// → Mempool +// +// ComplianceAnteWrapper 在标准 ante handler 之前运行合规检查: +// - OFAC 制裁名单地址拒绝 +// - Travel Rule ≥$3,000 身份数据预检查 +// - Structuring 拆分交易模式检测 +// +// 参考: 06-区块链开发指南.md §6 (链级合规能力) 和 §16 (验证节点级交易拦截) +func (app *GenexApp) setAnteHandler(txConfig client.TxConfig, maxGasWanted uint64) { + options := evmante.HandlerOptions{ + Cdc: app.appCodec, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + ExtensionOptionChecker: antetypes.HasDynamicFeeExtensionOption, + EvmKeeper: app.EVMKeeper, + FeegrantKeeper: app.FeeGrantKeeper, + IBCKeeper: app.IBCKeeper, + FeeMarketKeeper: app.FeeMarketKeeper, + SignModeHandler: txConfig.SignModeHandler(), + SigGasConsumer: evmante.SigVerificationGasConsumer, + MaxTxGasWanted: maxGasWanted, + DynamicFeeChecker: true, + PendingTxListener: app.onPendingTx, + } + if err := options.Validate(); err != nil { + panic(err) + } + + // 标准 cosmos/evm ante handler(签名/Gas/Nonce/EVM 路由) + standardHandler := evmante.NewAnteHandler(options) + + // 初始化 Genex 合规检查处理器 + app.ComplianceHandler = genexante.NewComplianceAnteHandler() + + // 将合规检查包装在标准 handler 之前 + // OFAC 名单和 Travel Rule 记录由链下 compliance-service 通过 + // app.ComplianceHandler.UpdateOFACList() / RecordTravelRule() 同步 + wrappedHandler := NewComplianceAnteWrapper(standardHandler, app.ComplianceHandler) + + app.SetAnteHandler(wrappedHandler) +} + +func (app *GenexApp) onPendingTx(hash common.Hash) { + for _, listener := range app.pendingTxListeners { + listener(hash) + } +} + +// RegisterPendingTxListener registers a pending tx listener for JSON-RPC +func (app *GenexApp) RegisterPendingTxListener(listener func(common.Hash)) { + app.pendingTxListeners = append(app.pendingTxListeners, listener) +} + +func (app *GenexApp) setPostHandler() { + postHandler, err := posthandler.NewPostHandler( + posthandler.HandlerOptions{}, + ) + if err != nil { + panic(err) + } + app.SetPostHandler(postHandler) +} + +// Name returns the name of the App +func (app *GenexApp) Name() string { return app.BaseApp.Name() } + +// BeginBlocker application updates every begin block +func (app *GenexApp) BeginBlocker(ctx sdk.Context) (sdk.BeginBlock, error) { + return app.ModuleManager.BeginBlock(ctx) +} + +// EndBlocker application updates every end block +func (app *GenexApp) EndBlocker(ctx sdk.Context) (sdk.EndBlock, error) { + return app.ModuleManager.EndBlock(ctx) +} + +// FinalizeBlock processes a block +func (app *GenexApp) FinalizeBlock(req *abci.RequestFinalizeBlock) (res *abci.ResponseFinalizeBlock, err error) { + return app.BaseApp.FinalizeBlock(req) +} + +// Configurator returns the app configurator +func (app *GenexApp) Configurator() module.Configurator { + return app.configurator +} + +// InitChainer application update at chain initialization +func (app *GenexApp) InitChainer(ctx sdk.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { + var genesisState GenesisState + if err := json.Unmarshal(req.AppStateBytes, &genesisState); err != nil { + panic(err) + } + if err := app.UpgradeKeeper.SetModuleVersionMap(ctx, app.ModuleManager.GetVersionMap()); err != nil { + panic(err) + } + return app.ModuleManager.InitGenesis(ctx, app.appCodec, genesisState) +} + +// PreBlocker runs before each block +func (app *GenexApp) PreBlocker(ctx sdk.Context, _ *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) { + return app.ModuleManager.PreBlock(ctx) +} + +// LoadHeight loads a particular height +func (app *GenexApp) LoadHeight(height int64) error { + return app.LoadVersion(height) +} + +// LegacyAmino returns GenexApp's amino codec +func (app *GenexApp) LegacyAmino() *codec.LegacyAmino { + return app.legacyAmino +} + +// AppCodec returns GenexApp's app codec +func (app *GenexApp) AppCodec() codec.Codec { + return app.appCodec +} + +// InterfaceRegistry returns GenexApp's InterfaceRegistry +func (app *GenexApp) InterfaceRegistry() types.InterfaceRegistry { + return app.interfaceRegistry +} + +// TxConfig returns GenexApp's TxConfig +func (app *GenexApp) TxConfig() client.TxConfig { + return app.txConfig +} + +// DefaultGenesis returns default genesis with Genex customizations +func (app *GenexApp) DefaultGenesis() map[string]json.RawMessage { + genesis := app.BasicModuleManager.DefaultGenesis(app.appCodec) + + // Genex: customize mint denom + mintGenState := NewMintGenesisState() + genesis[minttypes.ModuleName] = app.appCodec.MustMarshalJSON(mintGenState) + + // Genex: EVM genesis with precompiles + evmGenState := NewEVMGenesisState() + genesis[evmtypes.ModuleName] = app.appCodec.MustMarshalJSON(evmGenState) + + // Genex: FeeMarket genesis (NoBaseFee = true for gas subsidy) + feeMarketGenState := NewFeeMarketGenesisState() + genesis[feemarkettypes.ModuleName] = app.appCodec.MustMarshalJSON(feeMarketGenState) + + return genesis +} + +// GetKey returns the KVStoreKey for the provided store key +func (app *GenexApp) GetKey(storeKey string) *storetypes.KVStoreKey { + return app.keys[storeKey] +} + +// GetTKey returns the TransientStoreKey for the provided store key +func (app *GenexApp) GetTKey(storeKey string) *storetypes.TransientStoreKey { + return app.tkeys[storeKey] +} + +// GetMemKey returns the MemStoreKey for the provided mem key +func (app *GenexApp) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { + return app.memKeys[storeKey] +} + +// SimulationManager implements the SimulationApp interface +func (app *GenexApp) SimulationManager() *module.SimulationManager { + return app.sm +} + +// RegisterAPIRoutes registers all application module routes +func (app *GenexApp) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig) { + clientCtx := apiSvr.ClientCtx + authtx.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + cmtservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + node.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + app.BasicModuleManager.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + if err := sdkserver.RegisterSwaggerAPI(apiSvr.ClientCtx, apiSvr.Router, apiConfig.Swagger); err != nil { + panic(err) + } +} + +// RegisterTxService implements the Application.RegisterTxService method +func (app *GenexApp) RegisterTxService(clientCtx client.Context) { + authtx.RegisterTxService(app.GRPCQueryRouter(), clientCtx, app.Simulate, app.interfaceRegistry) +} + +// RegisterTendermintService implements the Application.RegisterTendermintService method +func (app *GenexApp) RegisterTendermintService(clientCtx client.Context) { + cmtservice.RegisterTendermintService(clientCtx, app.GRPCQueryRouter(), app.interfaceRegistry, app.Query) +} + +// RegisterNodeService registers the node gRPC service +func (app *GenexApp) RegisterNodeService(clientCtx client.Context, cfg config.Config) { + node.RegisterNodeService(clientCtx, app.GRPCQueryRouter(), cfg) +} + +// ============================================================ +// IBC Testing App interface implementations +// ============================================================ + +func (app *GenexApp) GetBaseApp() *baseapp.BaseApp { return app.BaseApp } +func (app *GenexApp) GetStakingKeeperSDK() stakingkeeper.Keeper { return *app.StakingKeeper } +func (app *GenexApp) GetIBCKeeper() *ibckeeper.Keeper { return app.IBCKeeper } +func (app *GenexApp) GetEVMKeeper() *evmkeeper.Keeper { return app.EVMKeeper } +func (app *GenexApp) GetErc20Keeper() *erc20keeper.Keeper { return &app.Erc20Keeper } +func (app *GenexApp) SetErc20Keeper(k erc20keeper.Keeper) { app.Erc20Keeper = k } +func (app *GenexApp) GetGovKeeper() govkeeper.Keeper { return app.GovKeeper } +func (app *GenexApp) GetEvidenceKeeper() *evidencekeeper.Keeper { return &app.EvidenceKeeper } +func (app *GenexApp) GetSlashingKeeper() slashingkeeper.Keeper { return app.SlashingKeeper } +func (app *GenexApp) GetBankKeeper() bankkeeper.Keeper { return app.BankKeeper } +func (app *GenexApp) GetFeeMarketKeeper() *feemarketkeeper.Keeper { return &app.FeeMarketKeeper } +func (app *GenexApp) GetFeeGrantKeeper() feegrantkeeper.Keeper { return app.FeeGrantKeeper } +func (app *GenexApp) GetConsensusParamsKeeper() consensusparamkeeper.Keeper { return app.ConsensusParamsKeeper } +func (app *GenexApp) GetAccountKeeper() authkeeper.AccountKeeper { return app.AccountKeeper } +func (app *GenexApp) GetAuthzKeeper() authzkeeper.Keeper { return app.AuthzKeeper } +func (app *GenexApp) GetDistrKeeper() distrkeeper.Keeper { return app.DistrKeeper } +func (app *GenexApp) GetStakingKeeper() *stakingkeeper.Keeper { return app.StakingKeeper } +func (app *GenexApp) GetMintKeeper() mintkeeper.Keeper { return app.MintKeeper } +func (app *GenexApp) GetPreciseBankKeeper() *precisebankkeeper.Keeper { return &app.PreciseBankKeeper } +func (app *GenexApp) GetCallbackKeeper() ibccallbackskeeper.ContractKeeper { return app.CallbackKeeper } +func (app *GenexApp) GetTransferKeeper() transferkeeper.Keeper { return app.TransferKeeper } +func (app *GenexApp) SetTransferKeeper(k transferkeeper.Keeper) { app.TransferKeeper = k } +func (app *GenexApp) GetMempool() sdkmempool.ExtMempool { return app.EVMMempool } +func (app *GenexApp) GetAnteHandler() sdk.AnteHandler { return app.BaseApp.AnteHandler() } +func (app *GenexApp) GetTxConfig() client.TxConfig { return app.txConfig } +func (app *GenexApp) SetClientCtx(clientCtx client.Context) { app.clientCtx = clientCtx } + +// Close gracefully shuts down the application +func (app *GenexApp) Close() error { + var err error + if m, ok := app.GetMempool().(*evmmempool.ExperimentalEVMMempool); ok { + app.Logger().Info("Shutting down mempool") + err = m.Close() + } + msg := "Genex Chain gracefully shutdown" + err = errors.Join(err, app.BaseApp.Close()) + if err == nil { + app.Logger().Info(msg) + } else { + app.Logger().Error(msg, "error", err) + } + return err +} + +// AutoCliOpts returns the autocli options for the app +func (app *GenexApp) AutoCliOpts() autocli.AppOptions { + modules := make(map[string]appmodule.AppModule, 0) + for _, m := range app.ModuleManager.Modules { + if moduleWithName, ok := m.(module.HasName); ok { + moduleName := moduleWithName.Name() + if appModule, ok := moduleWithName.(appmodule.AppModule); ok { + modules[moduleName] = appModule + } + } + } + return autocli.AppOptions{ + Modules: modules, + ModuleOptions: runtimeservices.ExtractAutoCLIOptions(app.ModuleManager.Modules), + AddressCodec: evmaddress.NewEvmCodec(GenexBech32Prefix), + ValidatorAddressCodec: evmaddress.NewEvmCodec(GenexBech32Prefix + "valoper"), + ConsensusAddressCodec: evmaddress.NewEvmCodec(GenexBech32Prefix + "valcons"), + } +} diff --git a/blockchain/genex-chain/cmd/genexd/cmd/root.go b/blockchain/genex-chain/cmd/genexd/cmd/root.go new file mode 100644 index 0000000..dbb87e0 --- /dev/null +++ b/blockchain/genex-chain/cmd/genexd/cmd/root.go @@ -0,0 +1,372 @@ +// Package cmd — Genex Chain CLI 命令 +package cmd + +import ( + "errors" + "io" + "os" + + "github.com/cosmos/evm/x/vm/types" + + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/spf13/cast" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + cmtcfg "github.com/cometbft/cometbft/config" + cmtcli "github.com/cometbft/cometbft/libs/cli" + + dbm "github.com/cosmos/cosmos-db" + cosmosevmcmd "github.com/cosmos/evm/client" + evmdebug "github.com/cosmos/evm/client/debug" + evmconfig "github.com/cosmos/evm/config" + "github.com/cosmos/evm/crypto/hd" + cosmosevmserver "github.com/cosmos/evm/server" + srvflags "github.com/cosmos/evm/server/flags" + + "cosmossdk.io/log" + "cosmossdk.io/store" + snapshottypes "cosmossdk.io/store/snapshots/types" + storetypes "cosmossdk.io/store/types" + confixcmd "cosmossdk.io/tools/confix/cmd" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + clientcfg "github.com/cosmos/cosmos-sdk/client/config" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/pruning" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/snapshot" + sdkserver "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + sdktestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + txmodule "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + + genexchain "github.com/genex/genex-chain" +) + +const ( + genexEVMChainID = 8888 +) + +// NewRootCmd creates the root command for genexd +func NewRootCmd() *cobra.Command { + // Pre-instantiate the application for encoding config + tempApp := genexchain.NewGenexApp( + log.NewNopLogger(), + dbm.NewMemDB(), + nil, + true, + simtestutil.EmptyAppOptions{}, + ) + + encodingConfig := sdktestutil.TestEncodingConfig{ + InterfaceRegistry: tempApp.InterfaceRegistry(), + Codec: tempApp.AppCodec(), + TxConfig: tempApp.GetTxConfig(), + Amino: tempApp.LegacyAmino(), + } + + initClientCtx := client.Context{}. + WithCodec(encodingConfig.Codec). + WithInterfaceRegistry(encodingConfig.InterfaceRegistry). + WithTxConfig(encodingConfig.TxConfig). + WithLegacyAmino(encodingConfig.Amino). + WithInput(os.Stdin). + WithAccountRetriever(authtypes.AccountRetriever{}). + WithBroadcastMode(flags.FlagBroadcastMode). + WithHomeDir(genexchain.GenexDefaultNodeHome()). + WithViper(""). + // Cosmos EVM specific setup + WithKeyringOptions(hd.EthSecp256k1Option()). + WithLedgerHasProtobuf(true) + + rootCmd := &cobra.Command{ + Use: "genexd", + Short: "Genex Chain — 券金融专用应用链", + Long: `Genex Chain 是基于 Cosmos SDK + cosmos/evm + CometBFT 构建的券金融专用应用链。 + +特性: + - 完全 EVM 兼容 (Solidity, Hardhat, MetaMask) + - CometBFT 即时终结性共识 (≤1s 出块) + - 链级合规 (OFAC, Travel Rule, KYC) + - 平台 Gas 全额补贴 (用户零 Gas) + - IBC 跨链 + ERC-20 代币桥接 + +EVM Chain ID: 8888 +Bech32 Prefix: genex +Bond Denom: agnx (GNX)`, + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + cmd.SetOut(cmd.OutOrStdout()) + cmd.SetErr(cmd.ErrOrStderr()) + + initClientCtx = initClientCtx.WithCmdContext(cmd.Context()) + initClientCtx, err := client.ReadPersistentCommandFlags(initClientCtx, cmd.Flags()) + if err != nil { + return err + } + + initClientCtx, err = clientcfg.ReadFromClientConfig(initClientCtx) + if err != nil { + return err + } + + // Enable SIGN_MODE_TEXTUAL when online + if !initClientCtx.Offline { + enabledSignModes := append(tx.DefaultSignModes, signing.SignMode_SIGN_MODE_TEXTUAL) + txConfigOpts := tx.ConfigOptions{ + EnabledSignModes: enabledSignModes, + TextualCoinMetadataQueryFn: txmodule.NewGRPCCoinMetadataQueryFn(initClientCtx), + } + txConfig, err := tx.NewTxConfigWithOptions(initClientCtx.Codec, txConfigOpts) + if err != nil { + return err + } + initClientCtx = initClientCtx.WithTxConfig(txConfig) + } + + if err := client.SetCmdClientContextHandler(initClientCtx, cmd); err != nil { + return err + } + + // Genex Chain config + customAppTemplate, customAppConfig := evmconfig.InitAppConfig( + types.DefaultEVMExtendedDenom, + genexEVMChainID, + ) + customTMConfig := initCometConfig() + + return sdkserver.InterceptConfigsPreRunHandler(cmd, customAppTemplate, customAppConfig, customTMConfig) + }, + } + + initRootCmd(rootCmd, tempApp) + + autoCliOpts := tempApp.AutoCliOpts() + initClientCtx, _ = clientcfg.ReadFromClientConfig(initClientCtx) + autoCliOpts.ClientCtx = initClientCtx + + if err := autoCliOpts.EnhanceRootCommand(rootCmd); err != nil { + panic(err) + } + + return rootCmd +} + +// initCometConfig returns CometBFT config optimized for Genex Chain +func initCometConfig() *cmtcfg.Config { + cfg := cmtcfg.DefaultConfig() + // Genex Chain: fast block times for better UX + cfg.Consensus.TimeoutCommit = 1_000_000_000 // 1s + return cfg +} + +func initRootCmd(rootCmd *cobra.Command, genexApp *genexchain.GenexApp) { + cfg := sdk.GetConfig() + cfg.Seal() + + defaultNodeHome := genexchain.GenexDefaultNodeHome() + sdkAppCreator := func(l log.Logger, d dbm.DB, w io.Writer, ao servertypes.AppOptions) servertypes.Application { + return newApp(l, d, w, ao) + } + + rootCmd.AddCommand( + genutilcli.InitCmd(genexApp.BasicModuleManager, defaultNodeHome), + genutilcli.Commands(genexApp.TxConfig(), genexApp.BasicModuleManager, defaultNodeHome), + cmtcli.NewCompletionCmd(rootCmd, true), + evmdebug.Cmd(), + confixcmd.ConfigCommand(), + pruning.Cmd(sdkAppCreator, defaultNodeHome), + snapshot.Cmd(sdkAppCreator), + ) + + // Add cosmos/evm server commands (start, tendermint, etc.) + cosmosevmserver.AddCommands( + rootCmd, + cosmosevmserver.NewDefaultStartOptions(newApp, defaultNodeHome), + appExport, + addModuleInitFlags, + ) + + // Add cosmos/evm key commands (with EthSecp256k1 support) + rootCmd.AddCommand( + cosmosevmcmd.KeyCommands(defaultNodeHome, true), + ) + + // Add status, query, tx commands + rootCmd.AddCommand( + sdkserver.StatusCommand(), + queryCommand(), + txCommand(), + ) + + // Add general tx flags + var err error + _, err = srvflags.AddTxFlags(rootCmd) + if err != nil { + panic(err) + } +} + +func addModuleInitFlags(_ *cobra.Command) {} + +func queryCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "query", + Aliases: []string{"q"}, + Short: "Querying subcommands", + DisableFlagParsing: false, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand( + rpc.QueryEventForTxCmd(), + rpc.ValidatorCommand(), + authcmd.QueryTxsByEventsCmd(), + authcmd.QueryTxCmd(), + sdkserver.QueryBlockCmd(), + sdkserver.QueryBlockResultsCmd(), + ) + cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID") + return cmd +} + +func txCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "tx", + Short: "Transactions subcommands", + DisableFlagParsing: false, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand( + authcmd.GetSignCommand(), + authcmd.GetSignBatchCommand(), + authcmd.GetMultiSignCommand(), + authcmd.GetMultiSignBatchCmd(), + authcmd.GetValidateSignaturesCommand(), + authcmd.GetBroadcastCommand(), + authcmd.GetEncodeCommand(), + authcmd.GetDecodeCommand(), + authcmd.GetSimulateCmd(), + ) + cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID") + return cmd +} + +// newApp creates the GenexApp with all configuration +func newApp( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + appOpts servertypes.AppOptions, +) cosmosevmserver.Application { + var cache storetypes.MultiStorePersistentCache + if cast.ToBool(appOpts.Get(sdkserver.FlagInterBlockCache)) { + cache = store.NewCommitKVStoreCacheManager() + } + + pruningOpts, err := sdkserver.GetPruningOptionsFromFlags(appOpts) + if err != nil { + panic(err) + } + + chainID, err := getChainIDFromOpts(appOpts) + if err != nil { + panic(err) + } + + snapshotStore, err := sdkserver.GetSnapshotStore(appOpts) + if err != nil { + panic(err) + } + + snapshotOptions := snapshottypes.NewSnapshotOptions( + cast.ToUint64(appOpts.Get(sdkserver.FlagStateSyncSnapshotInterval)), + cast.ToUint32(appOpts.Get(sdkserver.FlagStateSyncSnapshotKeepRecent)), + ) + + baseappOptions := []func(*baseapp.BaseApp){ + baseapp.SetPruning(pruningOpts), + baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(sdkserver.FlagMinGasPrices))), + baseapp.SetQueryGasLimit(cast.ToUint64(appOpts.Get(sdkserver.FlagQueryGasLimit))), + baseapp.SetHaltHeight(cast.ToUint64(appOpts.Get(sdkserver.FlagHaltHeight))), + baseapp.SetHaltTime(cast.ToUint64(appOpts.Get(sdkserver.FlagHaltTime))), + baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(sdkserver.FlagMinRetainBlocks))), + baseapp.SetInterBlockCache(cache), + baseapp.SetTrace(cast.ToBool(appOpts.Get(sdkserver.FlagTrace))), + baseapp.SetIndexEvents(cast.ToStringSlice(appOpts.Get(sdkserver.FlagIndexEvents))), + baseapp.SetSnapshot(snapshotStore, snapshotOptions), + baseapp.SetIAVLCacheSize(cast.ToInt(appOpts.Get(sdkserver.FlagIAVLCacheSize))), + baseapp.SetIAVLDisableFastNode(cast.ToBool(appOpts.Get(sdkserver.FlagDisableIAVLFastNode))), + baseapp.SetChainID(chainID), + } + + return genexchain.NewGenexApp( + logger, db, traceStore, true, + appOpts, + baseappOptions..., + ) +} + +// appExport exports state for genesis +func appExport( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + height int64, + forZeroHeight bool, + jailAllowedAddrs []string, + appOpts servertypes.AppOptions, + modulesToExport []string, +) (servertypes.ExportedApp, error) { + homePath, ok := appOpts.Get(flags.FlagHome).(string) + if !ok || homePath == "" { + return servertypes.ExportedApp{}, errors.New("application home not set") + } + + viperAppOpts, ok := appOpts.(*viper.Viper) + if !ok { + return servertypes.ExportedApp{}, errors.New("appOpts is not viper.Viper") + } + viperAppOpts.Set(sdkserver.FlagInvCheckPeriod, 1) + appOpts = viperAppOpts + + chainID, err := getChainIDFromOpts(appOpts) + if err != nil { + return servertypes.ExportedApp{}, err + } + + var genexApp *genexchain.GenexApp + if height != -1 { + genexApp = genexchain.NewGenexApp(logger, db, traceStore, false, appOpts, baseapp.SetChainID(chainID)) + if err := genexApp.LoadHeight(height); err != nil { + return servertypes.ExportedApp{}, err + } + } else { + genexApp = genexchain.NewGenexApp(logger, db, traceStore, true, appOpts, baseapp.SetChainID(chainID)) + } + + return genexApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) +} + +// getChainIDFromOpts returns the chain ID from app options +func getChainIDFromOpts(appOpts servertypes.AppOptions) (string, error) { + chainID := cast.ToString(appOpts.Get(flags.FlagChainID)) + if chainID == "" { + homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) + var err error + chainID, err = evmconfig.GetChainIDFromHome(homeDir) + if err != nil { + return "", err + } + } + return chainID, nil +} diff --git a/blockchain/genex-chain/cmd/genexd/main.go b/blockchain/genex-chain/cmd/genexd/main.go new file mode 100644 index 0000000..1be1233 --- /dev/null +++ b/blockchain/genex-chain/cmd/genexd/main.go @@ -0,0 +1,40 @@ +// Package main — Genex Chain 节点二进制入口 +// +// genexd 是 Genex Chain 的节点程序,基于 Cosmos SDK + cosmos/evm + CometBFT。 +// 支持完全 EVM 兼容,提供券 NFT 发行、交易、结算、合规的链上能力。 +// +// 用法: +// +// genexd init --chain-id genex-1 +// genexd start +// genexd keys add +// genexd version +package main + +import ( + "fmt" + "os" + + svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" + sdk "github.com/cosmos/cosmos-sdk/types" + + genexchain "github.com/genex/genex-chain" + "github.com/genex/genex-chain/cmd/genexd/cmd" +) + +func main() { + setupSDKConfig() + + rootCmd := cmd.NewRootCmd() + if err := svrcmd.Execute(rootCmd, "genexd", genexchain.GenexDefaultNodeHome()); err != nil { + fmt.Fprintln(rootCmd.OutOrStderr(), err) + os.Exit(1) + } +} + +// setupSDKConfig configures the Genex Chain SDK parameters +func setupSDKConfig() { + cfg := sdk.GetConfig() + genexchain.SetGenexBech32Prefixes(cfg) + cfg.Seal() +} diff --git a/blockchain/genex-chain/compliance_integration.go b/blockchain/genex-chain/compliance_integration.go new file mode 100644 index 0000000..5e84fcc --- /dev/null +++ b/blockchain/genex-chain/compliance_integration.go @@ -0,0 +1,149 @@ +// Package genexchain — Compliance Ante Handler Integration +// +// 本文件将独立的 ComplianceAnteHandler (x/evm/ante/) 桥接到 +// Cosmos SDK ante handler 链中,实现验证节点级交易拦截。 +// +// 对应指南: 06-区块链开发指南.md §6 (链级合规能力) 和 §16 (验证节点级交易拦截) +// +// 架构: +// +// TX → ComplianceAnteWrapper (OFAC/TravelRule/Structuring) +// → cosmos/evm AnteHandler (签名/Gas/Nonce/...) +// → Mempool → Block +// +// ComplianceAnteHandler 执行三项检查: +// 1. OFAC 地址拦截 — 制裁名单地址的交易直接拒绝,不进入 mempool +// 2. Structuring 检测 — 24h 内拆分交易规避 $3,000 阈值,标记为可疑 +// 3. Travel Rule 预打包检查 — ≥$3,000 转移必须已记录身份哈希 +// +// OFAC 名单由链下合规服务通过 ComplianceHandler.UpdateOFACList() 定期同步。 +// Travel Rule 数据由 compliance-service 通过 ComplianceHandler.RecordTravelRule() 写入。 +package genexchain + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/common" + + genexante "github.com/genex/genex-chain/x/evm/ante" +) + +// ComplianceAnteWrapper 将独立的 ComplianceAnteHandler 桥接到 Cosmos SDK ante 链。 +// +// 工作流程: +// 1. 遍历 tx 中的所有消息 +// 2. 对 MsgEthereumTx 类型的消息提取 from/to/value +// 3. 调用 ComplianceAnteHandler.AnteHandle() 执行合规检查 +// 4. 如果被拒绝 → 返回错误,交易不会被打包 +// 5. 如果可疑 → 记录日志,交易仍可打包 +// 6. 记录转移金额用于 Structuring 滑动窗口检测 +// 7. 合规通过后委托给标准 cosmos/evm ante handler +type ComplianceAnteWrapper struct { + compliance *genexante.ComplianceAnteHandler + inner sdk.AnteHandler +} + +// NewComplianceAnteWrapper 创建包含合规检查的 ante handler。 +// +// 参数: +// - inner: 标准 cosmos/evm ante handler (由 evmante.NewAnteHandler 创建) +// - compliance: Genex 合规检查处理器 +// +// 用法 (在 app.go 中): +// +// standardHandler := evmante.NewAnteHandler(options) +// complianceHandler := genexante.NewComplianceAnteHandler() +// wrappedHandler := NewComplianceAnteWrapper(standardHandler, complianceHandler) +// app.SetAnteHandler(wrappedHandler) +func NewComplianceAnteWrapper( + inner sdk.AnteHandler, + compliance *genexante.ComplianceAnteHandler, +) sdk.AnteHandler { + w := &ComplianceAnteWrapper{ + compliance: compliance, + inner: inner, + } + return w.AnteHandle +} + +// AnteHandle 在标准 ante handler 之前执行合规检查。 +// +// 对于非 EVM 的 Cosmos SDK 消息(如 staking、gov),合规检查被跳过, +// 直接委托给内部 handler。 +// +// 模拟模式 (simulate=true) 下跳过合规检查,因为 gas 估算不应被合规拒绝。 +func (w *ComplianceAnteWrapper) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, +) (sdk.Context, error) { + // 模拟模式跳过合规检查(gas 估算) + if simulate { + return w.inner(ctx, tx, simulate) + } + + for _, msg := range tx.GetMsgs() { + // 仅拦截 EVM 交易;Cosmos 原生消息直接放行 + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + continue + } + + // 获取底层 go-ethereum Transaction 对象 + ethTx := ethMsg.AsTransaction() + if ethTx == nil { + continue + } + + // 提取发送方 (from) — Cosmos AccAddress bytes → Ethereum hex + fromBytes := ethMsg.GetFrom() + fromAddr := common.BytesToAddress(fromBytes) + + // 提取接收方 (to) — 合约创建时为 nil + toHex := "" + if ethTx.To() != nil { + toHex = ethTx.To().Hex() + } + + // 提取交易金额 + value := ethTx.Value() + if value == nil { + value = new(big.Int) + } + + // ====== 执行合规检查 ====== + result := w.compliance.AnteHandle(fromAddr.Hex(), toHex, value) + + // 检查 1: OFAC / Travel Rule 拒绝 → 交易不进入 mempool + if !result.Allowed { + ctx.Logger().Error("Transaction rejected by compliance", + "module", "genex-compliance", + "from", fromAddr.Hex(), + "to", toHex, + "value", value.String(), + "reason", result.Reason, + ) + return ctx, fmt.Errorf("genex compliance: %s", result.Reason) + } + + // 检查 2: 可疑但允许 → 记录警告日志,交易仍可打包 + if result.Suspicious { + ctx.Logger().Warn("Suspicious transaction detected", + "module", "genex-compliance", + "from", fromAddr.Hex(), + "to", toHex, + "value", value.String(), + "reason", result.SuspReason, + ) + } + + // 检查 3: 记录转移金额用于 Structuring 滑动窗口检测 + if value.Sign() > 0 { + w.compliance.RecordTransfer(fromAddr.Hex(), value) + } + } + + // 合规通过,委托给标准 cosmos/evm ante handler + return w.inner(ctx, tx, simulate) +} diff --git a/blockchain/genex-chain/config/app.toml b/blockchain/genex-chain/config/app.toml new file mode 100644 index 0000000..7413c0a --- /dev/null +++ b/blockchain/genex-chain/config/app.toml @@ -0,0 +1,178 @@ +# Genex Chain — 应用层配置 +# 定义 EVM 模块、API、gRPC 等应用层参数 + +# ======================== +# 基本配置 +# ======================== +minimum-gas-prices = "0ugnx" # 平台全额补贴,用户零 Gas +pruning = "default" +pruning-keep-recent = "100" +pruning-keep-every = "0" +pruning-interval = "10" +halt-height = 0 +halt-time = 0 +min-retain-blocks = 0 + +# ======================== +# EVM 模块配置 +# ======================== +[evm] + # EVM 链 ID(MetaMask 等工具使用) + chain-id = 8888 + + # 最低 Gas 价格(0 = 平台补贴) + min-gas-price = "0" + + # EVM 交易跟踪(调试用) + tracer = "" + + # 最大交易 Gas 限制 + max-tx-gas-wanted = 0 + +# ======================== +# EVM Fee Market (EIP-1559) +# ======================== +[feemarket] + # 是否启用 EIP-1559 动态费用 + # 当前关闭,使用固定 0 Gas 价格 + no-base-fee = true + + # 基础费变化分母 + base-fee-change-denominator = 8 + + # 弹性乘数 + elasticity-multiplier = 2 + + # 初始基础费 + initial-base-fee = 0 + + # 最低 Gas 价格 + min-gas-price = "0" + +# ======================== +# JSON-RPC API(EVM 兼容) +# ======================== +[json-rpc] + # 是否启用 JSON-RPC(MetaMask, Hardhat, ethers.js 等工具需要) + enable = true + + # 监听地址 + address = "0.0.0.0:8545" + + # WebSocket 地址 + ws-address = "0.0.0.0:8546" + + # 启用的 API 命名空间 + api = "eth,txpool,personal,net,debug,web3" + + # Gas 上限(eth_call) + gas-cap = 25000000 + + # EVM 超时 + evm-timeout = "5s" + + # 交易费上限 + txfee-cap = 1 + + # 过滤器超时 + filter-cap = 200 + + # Fee History 上限 + feehistory-cap = 100 + + # 日志上限 + logs-cap = 10000 + block-range-cap = 10000 + + # HTTP 超时 + http-timeout = "30s" + http-idle-timeout = "120s" + + # 最大打开连接数 + max-open-connections = 0 + + # TLS + enable-indexer = false + +# ======================== +# Cosmos SDK REST API +# ======================== +[api] + enable = true + swagger = true + address = "tcp://0.0.0.0:1317" + max-open-connections = 1000 + rpc-read-timeout = 10 + rpc-write-timeout = 0 + rpc-max-body-bytes = 1000000 + enabled-unsafe-cors = false + +# ======================== +# gRPC +# ======================== +[grpc] + enable = true + address = "0.0.0.0:9090" + max-recv-msg-size = "10485760" + max-send-msg-size = "2147483647" + +# ======================== +# gRPC Web +# ======================== +[grpc-web] + enable = true + address = "0.0.0.0:9091" + enable-unsafe-cors = false + +# ======================== +# 状态流 +# ======================== +[state-streaming] + [state-streaming.file] + keys = ["*"] + write_dir = "" + prefix = "" + output-metadata = true + stop-node-on-error = true + fsync = false + +# ======================== +# 遥测 +# ======================== +[telemetry] + enabled = true + service-name = "genex-chain" + enable-hostname = false + enable-hostname-label = false + enable-service-label = false + prometheus-retention-time = 0 + global-labels = [] + +# ======================== +# 合规模块配置 +# ======================== +[compliance] + # 是否启用链级合规检查 + enabled = true + + # OFAC SDN 列表路径 + ofac-list-path = "data/ofac_sdn.json" + + # OFAC 列表自动更新间隔 + ofac-update-interval = "1h" + + # Travel Rule 阈值(USDC 精度 6 位小数) + travel-rule-threshold = 3000000000 + + # Structuring 检测窗口 + structuring-window = "24h" + + # 可疑交易上报端点 + suspicious-tx-report-url = "" + + # 监管 API + [compliance.regulatory-api] + enabled = false + address = "0.0.0.0:9200" + # API 密钥由监管机构单独分发 + require-auth = true diff --git a/blockchain/genex-chain/config/config.toml b/blockchain/genex-chain/config/config.toml new file mode 100644 index 0000000..529a022 --- /dev/null +++ b/blockchain/genex-chain/config/config.toml @@ -0,0 +1,141 @@ +# Genex Chain — CometBFT 节点配置 +# 生产验证节点配置 +# +# 文档: https://docs.cometbft.com/main/core/configuration + +# 基本配置 +proxy_app = "tcp://127.0.0.1:26658" +moniker = "genex-node" +fast_sync = true +db_backend = "goleveldb" +db_dir = "data" +log_level = "info" +log_format = "json" + +# ======================== +# 共识配置 +# ======================== +[consensus] + # 出块时间目标: ≤1 秒(CometBFT 即时终结性) + timeout_propose = "1s" + timeout_propose_delta = "500ms" + timeout_prevote = "500ms" + timeout_prevote_delta = "500ms" + timeout_precommit = "500ms" + timeout_precommit_delta = "500ms" + timeout_commit = "800ms" + + # 是否在收到 2/3 以上预投票后跳过 timeout + skip_timeout_commit = false + + # 创建空区块 + create_empty_blocks = true + create_empty_blocks_interval = "0s" + + # 双签证据 + double_sign_check_height = 0 + +# ======================== +# 交易内存池 +# ======================== +[mempool] + # 是否复播交易 + recheck = true + broadcast = true + + # 内存池大小 + size = 10000 + max_txs_bytes = 1073741824 # 1GB + max_tx_bytes = 1048576 # 1MB + + # 缓存大小 + cache_size = 10000 + + # 是否保持检查交易的顺序 + keep-invalid-txs-in-cache = false + +# ======================== +# P2P 网络 +# ======================== +[p2p] + laddr = "tcp://0.0.0.0:26656" + + # 外部地址(NAT 后面的节点需要设置) + external_address = "" + + # 种子节点 + seeds = "" + + # 持久连接的对等节点 + # 生产环境: "node1@us-east:26656,node2@sg:26656,node3@eu:26656" + persistent_peers = "" + + # UPNP 端口映射 + upnp = false + + # 地址簿 + addr_book_file = "config/addrbook.json" + addr_book_strict = true + + # 最大对等节点数 + max_num_inbound_peers = 100 + max_num_outbound_peers = 50 + + # 发送/接收速率限制 (字节/秒) + send_rate = 5120000 # 5 MB/s + recv_rate = 5120000 # 5 MB/s + + # 握手超时 + handshake_timeout = "20s" + dial_timeout = "3s" + +# ======================== +# RPC 服务 +# ======================== +[rpc] + laddr = "tcp://127.0.0.1:26657" + + # CORS 允许的域名 + cors_allowed_origins = [] + cors_allowed_methods = ["HEAD", "GET", "POST"] + cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"] + + # gRPC + grpc_laddr = "" + grpc_max_open_connections = 900 + + # 是否启用不安全的 RPC(仅开发环境) + unsafe = false + + # WebSocket + max_open_connections = 900 + max_subscription_clients = 100 + max_subscriptions_per_client = 5 + + # 超时 + timeout_broadcast_tx_commit = "10s" + +# ======================== +# 状态同步 +# ======================== +[statesync] + enable = false + rpc_servers = "" + trust_height = 0 + trust_hash = "" + trust_period = "168h0m0s" + +# ======================== +# 区块同步 +# ======================== +[blocksync] + version = "v0" + +# ======================== +# 遥测 +# ======================== +[instrumentation] + prometheus = true + prometheus_listen_addr = ":26660" + max_open_connections = 3 + namespace = "cometbft" diff --git a/blockchain/genex-chain/config/genesis.json b/blockchain/genex-chain/config/genesis.json new file mode 100644 index 0000000..6d64b8a --- /dev/null +++ b/blockchain/genex-chain/config/genesis.json @@ -0,0 +1,182 @@ +{ + "genesis_time": "2026-01-01T00:00:00Z", + "chain_id": "genex-8888", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "100000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": ["ed25519"] + }, + "version": { + "app": "0" + } + }, + "app_hash": "", + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "supply": [ + { + "denom": "ugnx", + "amount": "1000000000000000" + } + ], + "balances": [ + { + "_comment": "平台运营/Gas补贴池 (40%)", + "address": "genex1platform_operations_address", + "coins": [ + { "denom": "ugnx", "amount": "400000000000000" } + ] + }, + { + "_comment": "团队与顾问 (20%) — 4年线性释放,1年锁定", + "address": "genex1team_vesting_address", + "coins": [ + { "denom": "ugnx", "amount": "200000000000000" } + ] + }, + { + "_comment": "生态基金 (15%)", + "address": "genex1ecosystem_fund_address", + "coins": [ + { "denom": "ugnx", "amount": "150000000000000" } + ] + }, + { + "_comment": "未来融资预留 (15%)", + "address": "genex1future_financing_address", + "coins": [ + { "denom": "ugnx", "amount": "150000000000000" } + ] + }, + { + "_comment": "社区治理 DAO (10%)", + "address": "genex1community_dao_address", + "coins": [ + { "denom": "ugnx", "amount": "100000000000000" } + ] + } + ], + "denom_metadata": [ + { + "description": "Genex Chain 原生代币", + "denom_units": [ + { "denom": "ugnx", "exponent": 0, "aliases": ["micrognx"] }, + { "denom": "mgnx", "exponent": 3, "aliases": ["millignx"] }, + { "denom": "gnx", "exponent": 6, "aliases": ["GNX"] } + ], + "base": "ugnx", + "display": "gnx", + "name": "Genex Token", + "symbol": "GNX" + } + ] + }, + "staking": { + "params": { + "unbonding_time": "1814400s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "ugnx", + "min_commission_rate": "0.050000000000000000" + }, + "validators": [] + }, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + } + }, + "gov": { + "params": { + "min_deposit": [ + { "denom": "ugnx", "amount": "10000000" } + ], + "max_deposit_period": "172800s", + "voting_period": "172800s", + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000" + } + }, + "evm": { + "params": { + "evm_denom": "ugnx", + "enable_create": true, + "enable_call": true, + "extra_eips": [], + "chain_config": { + "chain_id": "8888", + "homestead_block": "0", + "dao_fork_block": "0", + "dao_fork_support": true, + "eip150_block": "0", + "eip150_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155_block": "0", + "eip158_block": "0", + "byzantium_block": "0", + "constantinople_block": "0", + "petersburg_block": "0", + "istanbul_block": "0", + "muir_glacier_block": "0", + "berlin_block": "0", + "london_block": "0", + "arrow_glacier_block": "0", + "gray_glacier_block": "0", + "merge_netsplit_block": "0", + "shanghai_time": "0" + } + }, + "state": [] + }, + "feemarket": { + "params": { + "no_base_fee": true, + "base_fee_change_denominator": 8, + "elasticity_multiplier": 2, + "initial_base_fee": "0", + "min_gas_price": "0.000000000000000000" + }, + "block_gas": "0" + }, + "compliance": { + "_comment": "Genex 自定义合规模块创世状态", + "params": { + "enabled": true, + "travel_rule_threshold": "3000000000", + "structuring_window": "86400", + "ofac_update_interval": "3600" + }, + "blacklisted_addresses": [], + "travel_rule_records": [] + } + } +} diff --git a/blockchain/genex-chain/export.go b/blockchain/genex-chain/export.go new file mode 100644 index 0000000..00dc711 --- /dev/null +++ b/blockchain/genex-chain/export.go @@ -0,0 +1,209 @@ +package genexchain + +import ( + "encoding/json" + "fmt" + "log" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + storetypes "cosmossdk.io/store/types" + + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// ExportAppStateAndValidators exports the state of the application for a genesis file +func (app *GenexApp) ExportAppStateAndValidators( + forZeroHeight bool, + jailAllowedAddrs []string, + modulesToExport []string, +) (servertypes.ExportedApp, error) { + ctx := app.NewContextLegacy(true, tmproto.Header{Height: app.LastBlockHeight()}) + + height := app.LastBlockHeight() + 1 + if forZeroHeight { + height = 0 + if err := app.prepForZeroHeightGenesis(ctx, jailAllowedAddrs); err != nil { + return servertypes.ExportedApp{}, err + } + } + + genState, err := app.ModuleManager.ExportGenesisForModules(ctx, app.appCodec, modulesToExport) + if err != nil { + return servertypes.ExportedApp{}, err + } + + appState, err := json.MarshalIndent(genState, "", " ") + if err != nil { + return servertypes.ExportedApp{}, err + } + + validators, err := staking.WriteValidators(ctx, app.StakingKeeper) + return servertypes.ExportedApp{ + AppState: appState, + Validators: validators, + Height: height, + ConsensusParams: app.GetConsensusParams(ctx), + }, err +} + +// prepForZeroHeightGenesis prepares for a fresh start at zero height +func (app *GenexApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []string) error { + applyAllowedAddrs := len(jailAllowedAddrs) > 0 + allowedAddrsMap := make(map[string]bool) + for _, addr := range jailAllowedAddrs { + _, err := sdk.ValAddressFromBech32(addr) + if err != nil { + log.Fatal(err) + } + allowedAddrsMap[addr] = true + } + + // Withdraw all validator commission + if err := app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { + _, _ = app.DistrKeeper.WithdrawValidatorCommission(ctx, sdk.ValAddress(val.GetOperator())) + return false + }); err != nil { + return err + } + + // Withdraw all delegator rewards + dels, err := app.StakingKeeper.GetAllDelegations(ctx) + if err != nil { + return err + } + for _, delegation := range dels { + valAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress) + if err != nil { + panic(err) + } + delAddr := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + _, _ = app.DistrKeeper.WithdrawDelegationRewards(ctx, delAddr, valAddr) + } + + // Reinitialize all delegations + for _, del := range dels { + valAddr, err := sdk.ValAddressFromBech32(del.ValidatorAddress) + if err != nil { + panic(err) + } + delAddr := sdk.MustAccAddressFromBech32(del.DelegatorAddress) + if err := app.DistrKeeper.Hooks().BeforeDelegationCreated(ctx, delAddr, valAddr); err != nil { + panic(fmt.Errorf("error while incrementing period: %w", err)) + } + if err := app.DistrKeeper.Hooks().AfterDelegationModified(ctx, delAddr, valAddr); err != nil { + panic(fmt.Errorf("error while creating a new delegation period record: %w", err)) + } + } + + app.DistrKeeper.DeleteAllValidatorSlashEvents(ctx) + app.DistrKeeper.DeleteAllValidatorHistoricalRewards(ctx) + + height := ctx.BlockHeight() + ctx = ctx.WithBlockHeight(0) + + // Reinitialize all validators + err = app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { + scraps, err := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, sdk.ValAddress(val.GetOperator())) + if err != nil { + return true + } + feePool, err := app.DistrKeeper.FeePool.Get(ctx) + if err != nil { + return true + } + feePool.CommunityPool = feePool.CommunityPool.Add(scraps...) + err = app.DistrKeeper.FeePool.Set(ctx, feePool) + if err != nil { + return true + } + err = app.DistrKeeper.Hooks().AfterValidatorCreated(ctx, sdk.ValAddress(val.GetOperator())) + return err != nil + }) + if err != nil { + return err + } + + ctx = ctx.WithBlockHeight(height) + + // Reset redelegations + var iterErr error + if err := app.StakingKeeper.IterateRedelegations(ctx, func(_ int64, red stakingtypes.Redelegation) (stop bool) { + for i := range red.Entries { + red.Entries[i].CreationHeight = 0 + } + if iterErr = app.StakingKeeper.SetRedelegation(ctx, red); iterErr != nil { + return true + } + return false + }); err != nil { + return err + } + if iterErr != nil { + return iterErr + } + + // Reset unbonding delegations + if err := app.StakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd stakingtypes.UnbondingDelegation) (stop bool) { + for i := range ubd.Entries { + ubd.Entries[i].CreationHeight = 0 + } + if iterErr = app.StakingKeeper.SetUnbondingDelegation(ctx, ubd); iterErr != nil { + return true + } + return false + }); err != nil { + return err + } + if iterErr != nil { + return iterErr + } + + // Reset validators + store := ctx.KVStore(app.GetKey(stakingtypes.StoreKey)) + iter := storetypes.KVStoreReversePrefixIterator(store, stakingtypes.ValidatorsKey) + counter := int16(0) + for ; iter.Valid(); iter.Next() { + addr := sdk.ValAddress(stakingtypes.AddressFromValidatorsKey(iter.Key())) + validator, err := app.StakingKeeper.GetValidator(ctx, addr) + if err != nil { + return fmt.Errorf("expected validator %s not found: %w", addr, err) + } + validator.UnbondingHeight = 0 + if applyAllowedAddrs && !allowedAddrsMap[addr.String()] { + validator.Jailed = true + } + if err = app.StakingKeeper.SetValidator(ctx, validator); err != nil { + return err + } + counter++ + } + if err := iter.Close(); err != nil { + return err + } + + _, err = app.StakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) + if err != nil { + log.Fatal(err) + } + + // Reset signing infos + if err := app.SlashingKeeper.IterateValidatorSigningInfos( + ctx, + func(addr sdk.ConsAddress, info slashingtypes.ValidatorSigningInfo) (stop bool) { + info.StartHeight = 0 + if iterErr = app.SlashingKeeper.SetValidatorSigningInfo(ctx, addr, info); iterErr != nil { + return true + } + return false + }, + ); err != nil { + return err + } + + return iterErr +} diff --git a/blockchain/genex-chain/genesis.go b/blockchain/genex-chain/genesis.go new file mode 100644 index 0000000..6377990 --- /dev/null +++ b/blockchain/genex-chain/genesis.go @@ -0,0 +1,36 @@ +package genexchain + +import ( + "encoding/json" + + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +// GenesisState defines the genesis state of the application +type GenesisState map[string]json.RawMessage + +// NewEVMGenesisState returns the default EVM genesis state with Genex customizations +func NewEVMGenesisState() *evmtypes.GenesisState { + evmGenState := evmtypes.DefaultGenesisState() + evmGenState.Params.ActiveStaticPrecompiles = evmtypes.AvailableStaticPrecompiles + evmGenState.Preinstalls = evmtypes.DefaultPreinstalls + return evmGenState +} + +// NewMintGenesisState returns the mint genesis state with Genex denom +func NewMintGenesisState() *minttypes.GenesisState { + mintGenState := minttypes.DefaultGenesisState() + mintGenState.Params.MintDenom = GenexBondDenom // agnx + return mintGenState +} + +// NewFeeMarketGenesisState returns the fee market genesis state +// NoBaseFee = true implements Genex's gas subsidy model (users pay zero gas) +func NewFeeMarketGenesisState() *feemarkettypes.GenesisState { + feeMarketGenState := feemarkettypes.DefaultGenesisState() + feeMarketGenState.Params.NoBaseFee = true + return feeMarketGenState +} diff --git a/blockchain/genex-chain/go.mod b/blockchain/genex-chain/go.mod new file mode 100644 index 0000000..a25cfde --- /dev/null +++ b/blockchain/genex-chain/go.mod @@ -0,0 +1,40 @@ +module github.com/genex/genex-chain + +go 1.23.8 + +require ( + cosmossdk.io/api v0.9.2 + cosmossdk.io/client/v2 v2.0.0-beta.7 + cosmossdk.io/core v0.11.3 + cosmossdk.io/errors v1.0.2 + cosmossdk.io/log v1.6.1 + cosmossdk.io/math v1.5.3 + cosmossdk.io/store v1.1.2 + cosmossdk.io/tools/confix v0.1.2 + cosmossdk.io/x/evidence v0.2.0 + cosmossdk.io/x/feegrant v0.2.0 + cosmossdk.io/x/upgrade v0.2.0 + github.com/cometbft/cometbft v0.38.19 + github.com/cosmos/cosmos-db v1.1.3 + github.com/cosmos/cosmos-sdk v0.53.5-0.20251030204916-768cb210885c + github.com/cosmos/evm v0.5.1 + github.com/cosmos/gogoproto v1.7.2 + github.com/cosmos/ibc-go/v10 v10.3.1-0.20250909102629-ed3b125c7b6f + github.com/ethereum/go-ethereum v1.15.11 + github.com/spf13/cast v1.10.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.20.1 + google.golang.org/grpc v1.75.0 +) + +replace ( + // Use cosmos fork of keyring + github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 + // Use Cosmos geth fork (branch: release/1.16) + github.com/ethereum/go-ethereum => github.com/cosmos/go-ethereum v1.16.2-cosmos-1 + // Security Advisory https://github.com/advisories/GHSA-h395-qcrw-5vmq + github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.1 + // Replace broken goleveldb + github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 +) diff --git a/blockchain/genex-chain/interfaces.go b/blockchain/genex-chain/interfaces.go new file mode 100644 index 0000000..3b87912 --- /dev/null +++ b/blockchain/genex-chain/interfaces.go @@ -0,0 +1,12 @@ +package genexchain + +import ( + cmn "github.com/cosmos/evm/precompiles/common" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +// BankKeeper defines the banking contract that must be fulfilled +type BankKeeper interface { + evmtypes.BankKeeper + cmn.BankKeeper +} diff --git a/blockchain/genex-chain/mempool.go b/blockchain/genex-chain/mempool.go new file mode 100644 index 0000000..e2bab6c --- /dev/null +++ b/blockchain/genex-chain/mempool.go @@ -0,0 +1,69 @@ +package genexchain + +import ( + "fmt" + + "cosmossdk.io/log" + + "github.com/cosmos/cosmos-sdk/baseapp" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + + evmconfig "github.com/cosmos/evm/config" + evmmempool "github.com/cosmos/evm/mempool" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +// configureEVMMempool sets up the EVM mempool and related handlers +func (app *GenexApp) configureEVMMempool(appOpts servertypes.AppOptions, logger log.Logger) error { + if evmtypes.GetChainConfig() == nil { + logger.Debug("evm chain config is not set, skipping mempool configuration") + return nil + } + + cosmosPoolMaxTx := evmconfig.GetCosmosPoolMaxTx(appOpts, logger) + if cosmosPoolMaxTx < 0 { + logger.Debug("app-side mempool is disabled, skipping evm mempool configuration") + return nil + } + + mempoolConfig, err := app.createMempoolConfig(appOpts, logger) + if err != nil { + return fmt.Errorf("failed to get mempool config: %w", err) + } + + evmMempool := evmmempool.NewExperimentalEVMMempool( + app.CreateQueryContext, + logger, + app.EVMKeeper, + app.FeeMarketKeeper, + app.txConfig, + app.clientCtx, + mempoolConfig, + cosmosPoolMaxTx, + ) + app.EVMMempool = evmMempool + app.SetMempool(evmMempool) + checkTxHandler := evmmempool.NewCheckTxHandler(evmMempool) + app.SetCheckTxHandler(checkTxHandler) + + abciProposalHandler := baseapp.NewDefaultProposalHandler(evmMempool, app) + abciProposalHandler.SetSignerExtractionAdapter( + evmmempool.NewEthSignerExtractionAdapter( + sdkmempool.NewDefaultSignerExtractionAdapter(), + ), + ) + app.SetPrepareProposal(abciProposalHandler.PrepareProposalHandler()) + + return nil +} + +// createMempoolConfig creates EVMMempoolConfig from app options +func (app *GenexApp) createMempoolConfig(appOpts servertypes.AppOptions, logger log.Logger) (*evmmempool.EVMMempoolConfig, error) { + return &evmmempool.EVMMempoolConfig{ + AnteHandler: app.GetAnteHandler(), + LegacyPoolConfig: evmconfig.GetLegacyPoolConfig(appOpts, logger), + BlockGasLimit: evmconfig.GetBlockGasLimit(appOpts, logger), + MinTip: evmconfig.GetMinTip(appOpts, logger), + }, nil +} diff --git a/blockchain/genex-chain/scripts/build-production.sh b/blockchain/genex-chain/scripts/build-production.sh new file mode 100644 index 0000000..bfdd2b5 --- /dev/null +++ b/blockchain/genex-chain/scripts/build-production.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Genex Chain — 生产构建脚本 +# +# 基于 cosmos/evm v0.5.1, Cosmos SDK v0.53.5, CometBFT v0.38.19 +# +# 方式1: 直接构建(需要 Linux/macOS + Go 1.23.8+ + CGO) +# bash scripts/build-production.sh +# +# 方式2: Docker 构建(推荐,任何平台) +# docker build -t genex-chain:latest . + +set -euo pipefail + +echo "============================================" +echo " Genex Chain — Production Build" +echo " cosmos/evm v0.5.1 | Cosmos SDK v0.53.5" +echo "============================================" + +BINARY_NAME="genexd" +VERSION="1.0.0" +CHAIN_ID="genex-1" +EVM_CHAIN_ID=8888 +COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "dev") +BUILD_DIR="./build" + +# ============================ +# Step 1: Check prerequisites +# ============================ +echo "" +echo "[1/4] Checking prerequisites..." + +# Check Go version +if ! command -v go &> /dev/null; then + echo "ERROR: Go is not installed. Minimum required: Go 1.23.8" + echo "Install from: https://go.dev/dl/" + exit 1 +fi + +GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//') +echo " Go version: $GO_VERSION" + +# Check CGO +if [ "$(go env CGO_ENABLED)" != "1" ]; then + echo "WARNING: CGO is disabled. Enabling..." + export CGO_ENABLED=1 +fi + +# Check C compiler +if ! command -v gcc &> /dev/null && ! command -v cc &> /dev/null; then + echo "ERROR: C compiler not found. Install gcc or clang." + echo " Ubuntu/Debian: sudo apt install build-essential" + echo " macOS: xcode-select --install" + echo " Alpine: apk add gcc musl-dev" + exit 1 +fi +echo " CGO: enabled" +echo " C compiler: $(gcc --version 2>/dev/null | head -1 || cc --version 2>/dev/null | head -1)" + +# ============================ +# Step 2: Resolve dependencies +# ============================ +echo "" +echo "[2/4] Resolving dependencies..." +go mod tidy +go mod download +echo " Dependencies resolved." + +# ============================ +# Step 3: Build binary +# ============================ +echo "" +echo "[3/4] Building binary..." + +LDFLAGS="-X github.com/cosmos/cosmos-sdk/version.Name=GenexChain \ + -X github.com/cosmos/cosmos-sdk/version.AppName=$BINARY_NAME \ + -X github.com/cosmos/cosmos-sdk/version.Version=$VERSION \ + -X github.com/cosmos/cosmos-sdk/version.Commit=$COMMIT \ + -X github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger \ + -w -s" + +mkdir -p "$BUILD_DIR" + +CGO_ENABLED=1 go build \ + -ldflags "$LDFLAGS" \ + -tags "netgo,ledger" \ + -trimpath \ + -o "$BUILD_DIR/$BINARY_NAME" \ + ./cmd/genexd/ + +echo " Binary: $BUILD_DIR/$BINARY_NAME" +echo " Size: $(du -h "$BUILD_DIR/$BINARY_NAME" | awk '{print $1}')" + +# ============================ +# Step 4: Verify build +# ============================ +echo "" +echo "[4/4] Verifying build..." +$BUILD_DIR/$BINARY_NAME version +echo " Build verified." + +# Run module tests +echo "" +echo "Running module tests..." +go test -v ./x/evm/ante/ ./x/evm/keeper/ + +echo "" +echo "============================================" +echo " Production Build Complete!" +echo "============================================" +echo "" +echo " Binary: $BUILD_DIR/$BINARY_NAME" +echo " Version: $VERSION ($COMMIT)" +echo " Chain ID: $CHAIN_ID" +echo " EVM Chain ID: $EVM_CHAIN_ID" +echo " Bech32: genex" +echo " Denom: agnx (GNX)" +echo "" +echo " Quick Start:" +echo " 1. Init: $BUILD_DIR/$BINARY_NAME init my-node --chain-id $CHAIN_ID" +echo " 2. Start: $BUILD_DIR/$BINARY_NAME start --minimum-gas-prices 0agnx --evm.chain-id $EVM_CHAIN_ID" +echo "" +echo " Docker Deployment:" +echo " docker build -t genex-chain:$VERSION ." +echo " docker compose -f blockchain/docker-compose.yml up -d" +echo "" +echo "============================================" diff --git a/blockchain/genex-chain/scripts/init-local.sh b/blockchain/genex-chain/scripts/init-local.sh new file mode 100644 index 0000000..472879c --- /dev/null +++ b/blockchain/genex-chain/scripts/init-local.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# Genex Chain — 本地单节点测试链初始化脚本 +# +# 基于 cosmos/evm v0.5.1, Cosmos SDK v0.53.5 +# +# 用法: bash scripts/init-local.sh +# 或: make init-local +# +# 此脚本将: +# 1. 编译 genexd 二进制 +# 2. 初始化本地节点 +# 3. 创建验证者账户 +# 4. 配置创世状态(GNX 代币分配) +# 5. 启动本地链 + +set -euo pipefail + +BINARY="./build/genexd" +CHAIN_ID="genex-localnet" +MONIKER="local-validator" +HOME_DIR="$HOME/.genexd" +DENOM="agnx" +KEYRING="test" +EVM_CHAIN_ID=8888 + +echo "============================================" +echo " Genex Chain — Local Node Initialization" +echo " cosmos/evm v0.5.1 | EVM Chain ID: $EVM_CHAIN_ID" +echo "============================================" + +# Step 1: Build +echo "" +echo "[1/6] Building genexd..." +make build + +# Step 2: Clean old data +echo "[2/6] Cleaning previous data..." +rm -rf "$HOME_DIR" + +# Step 3: Initialize node +echo "[3/6] Initializing node..." +$BINARY init "$MONIKER" --chain-id "$CHAIN_ID" + +# Step 4: Create accounts +echo "[4/6] Creating accounts..." + +# Validator account +echo " Creating validator account..." +$BINARY keys add validator --keyring-backend "$KEYRING" 2>&1 | tee /tmp/genex-validator-key.txt +VALIDATOR_ADDR=$($BINARY keys show validator -a --keyring-backend "$KEYRING") +echo " Validator address: $VALIDATOR_ADDR" + +# Platform operations account (Gas subsidy pool) +echo " Creating platform account..." +$BINARY keys add platform --keyring-backend "$KEYRING" 2>&1 | tee /tmp/genex-platform-key.txt +PLATFORM_ADDR=$($BINARY keys show platform -a --keyring-backend "$KEYRING") +echo " Platform address: $PLATFORM_ADDR" + +# Test user accounts +echo " Creating test user accounts..." +$BINARY keys add user1 --keyring-backend "$KEYRING" > /dev/null 2>&1 +$BINARY keys add user2 --keyring-backend "$KEYRING" > /dev/null 2>&1 +$BINARY keys add issuer --keyring-backend "$KEYRING" > /dev/null 2>&1 + +# Step 5: Configure genesis +echo "[5/6] Configuring genesis..." + +# Validator: 100M GNX (100M * 10^18 agnx) +$BINARY genesis add-genesis-account validator "100000000000000000000000000${DENOM}" --keyring-backend "$KEYRING" +# Platform: 400M GNX (Gas subsidy pool) +$BINARY genesis add-genesis-account platform "400000000000000000000000000${DENOM}" --keyring-backend "$KEYRING" +# Test users: 1M GNX each +$BINARY genesis add-genesis-account user1 "1000000000000000000000000${DENOM}" --keyring-backend "$KEYRING" +$BINARY genesis add-genesis-account user2 "1000000000000000000000000${DENOM}" --keyring-backend "$KEYRING" +$BINARY genesis add-genesis-account issuer "10000000000000000000000000${DENOM}" --keyring-backend "$KEYRING" + +# Create genesis validator tx +$BINARY genesis gentx validator "50000000000000000000000000${DENOM}" \ + --chain-id "$CHAIN_ID" \ + --moniker "$MONIKER" \ + --commission-rate "0.10" \ + --commission-max-rate "0.20" \ + --commission-max-change-rate "0.01" \ + --min-self-delegation "1" \ + --keyring-backend "$KEYRING" + +# Collect genesis txs +$BINARY genesis collect-gentxs + +# Validate genesis +$BINARY genesis validate-genesis + +# Step 6: Configuration tuning +echo "[6/6] Applying configurations..." + +# Set minimum-gas-prices = 0 (platform subsidy) +sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0agnx"/' "$HOME_DIR/config/app.toml" + +# Enable JSON-RPC (EVM) +sed -i 's/enable = false/enable = true/' "$HOME_DIR/config/app.toml" + +# Set EVM chain ID +sed -i "s/evm-chain-id = .*/evm-chain-id = $EVM_CHAIN_ID/" "$HOME_DIR/config/app.toml" + +echo "" +echo "============================================" +echo " Initialization Complete!" +echo "============================================" +echo "" +echo "Chain ID: $CHAIN_ID" +echo "Moniker: $MONIKER" +echo "Home: $HOME_DIR" +echo "EVM Chain: $EVM_CHAIN_ID" +echo "Denom: $DENOM (GNX)" +echo "Bech32: genex" +echo "" +echo "Endpoints:" +echo " EVM RPC: http://localhost:8545" +echo " EVM WS: ws://localhost:8546" +echo " Cosmos RPC: http://localhost:26657" +echo " REST API: http://localhost:1317" +echo " gRPC: localhost:9090" +echo "" +echo "Validator: $VALIDATOR_ADDR" +echo "Platform: $PLATFORM_ADDR" +echo "" +echo "Start: make start" +echo " or: $BINARY start --evm.chain-id $EVM_CHAIN_ID --minimum-gas-prices 0agnx" +echo "" +echo "MetaMask Config:" +echo " Network: Genex Chain (Local)" +echo " RPC URL: http://localhost:8545" +echo " Chain ID: $EVM_CHAIN_ID" +echo " Currency: GNX" +echo "" +echo "Keys saved to /tmp/genex-*-key.txt" +echo "============================================" diff --git a/blockchain/genex-chain/scripts/init-testnet.sh b/blockchain/genex-chain/scripts/init-testnet.sh new file mode 100644 index 0000000..9d895d8 --- /dev/null +++ b/blockchain/genex-chain/scripts/init-testnet.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# Genex Chain — 多验证节点测试网初始化脚本 +# +# 用法: bash scripts/init-testnet.sh +# 或: make init-testnet +# +# 生成 5 个验证节点配置: +# genex-us-east-1 (创世节点) +# genex-us-west-1 (创世节点) +# genex-sg-1 (创世节点) +# genex-inst-1 (机构节点) +# genex-inst-2 (机构节点) +# +# 另外生成 1 个监管只读节点: +# genex-regulatory-1 + +set -euo pipefail + +BINARY="./build/genexd" +CHAIN_ID="genex-testnet-1" +DENOM="agnx" # atto GNX, 18 decimals (EVM 兼容) +KEYRING="test" +BASE_DIR="./testnet" + +# 节点配置 +NODES=( + "genex-us-east-1:genesis:26656:26657:8545" + "genex-us-west-1:genesis:26666:26667:8555" + "genex-sg-1:genesis:26676:26677:8565" + "genex-inst-1:institution:26686:26687:8575" + "genex-inst-2:institution:26696:26697:8585" +) + +echo "============================================" +echo " Genex Chain — Testnet Initialization" +echo " ${#NODES[@]} Validator Nodes + 1 Regulatory" +echo "============================================" + +# 编译 +echo "" +echo "[1/7] Building genexd..." +make build + +# 清理 +echo "[2/7] Cleaning previous testnet data..." +rm -rf "$BASE_DIR" +mkdir -p "$BASE_DIR" + +# 初始化所有节点 +echo "[3/7] Initializing nodes..." +for node_config in "${NODES[@]}"; do + IFS=':' read -r moniker type p2p_port rpc_port evm_port <<< "$node_config" + node_home="$BASE_DIR/$moniker" + + echo " Initializing $moniker ($type)..." + $BINARY init "$moniker" --chain-id "$CHAIN_ID" --home "$node_home" > /dev/null 2>&1 + + # 创建验证者密钥 + $BINARY keys add validator --keyring-backend "$KEYRING" --home "$node_home" > /dev/null 2>&1 +done + +# 收集所有验证者地址 +echo "[4/7] Creating genesis accounts..." +GENESIS_HOME="$BASE_DIR/genex-us-east-1" + +for node_config in "${NODES[@]}"; do + IFS=':' read -r moniker type p2p_port rpc_port evm_port <<< "$node_config" + node_home="$BASE_DIR/$moniker" + + ADDR=$($BINARY keys show validator -a --keyring-backend "$KEYRING" --home "$node_home") + + # 不同节点类型不同质押量 (18 decimals: N * 10^18) + if [ "$type" = "genesis" ]; then + AMOUNT="100000000000000000000000000${DENOM}" # 100M GNX (10^8 * 10^18) + else + AMOUNT="50000000000000000000000000${DENOM}" # 50M GNX (5*10^7 * 10^18) + fi + + $BINARY genesis add-genesis-account "$ADDR" "$AMOUNT" --home "$GENESIS_HOME" --keyring-backend "$KEYRING" + echo " $moniker ($type): $ADDR — $AMOUNT" +done + +# 平台运营账户 +echo " Adding platform operations account..." +$BINARY keys add platform --keyring-backend "$KEYRING" --home "$GENESIS_HOME" > /dev/null 2>&1 +PLATFORM_ADDR=$($BINARY keys show platform -a --keyring-backend "$KEYRING" --home "$GENESIS_HOME") +$BINARY genesis add-genesis-account "$PLATFORM_ADDR" "400000000000000000000000000${DENOM}" --home "$GENESIS_HOME" # 400M GNX + +# 创建 GenTx +echo "[5/7] Creating genesis transactions..." +for node_config in "${NODES[@]}"; do + IFS=':' read -r moniker type p2p_port rpc_port evm_port <<< "$node_config" + node_home="$BASE_DIR/$moniker" + + if [ "$type" = "genesis" ]; then + STAKE="50000000000000000000000000${DENOM}" # 50M GNX + else + STAKE="25000000000000000000000000${DENOM}" # 25M GNX + fi + + $BINARY genesis gentx validator "$STAKE" \ + --chain-id "$CHAIN_ID" \ + --moniker "$moniker" \ + --commission-rate "0.10" \ + --commission-max-rate "0.20" \ + --commission-max-change-rate "0.01" \ + --min-self-delegation "1" \ + --keyring-backend "$KEYRING" \ + --home "$node_home" + + # 复制 gentx 到主节点 + cp "$node_home/config/gentx/"*.json "$GENESIS_HOME/config/gentx/" 2>/dev/null || true +done + +# 收集 GenTx +echo "[6/7] Collecting genesis transactions..." +$BINARY genesis collect-gentxs --home "$GENESIS_HOME" +$BINARY genesis validate-genesis --home "$GENESIS_HOME" + +# 分发创世文件和配置 +echo "[7/7] Distributing genesis and configuring peers..." +PERSISTENT_PEERS="" +for node_config in "${NODES[@]}"; do + IFS=':' read -r moniker type p2p_port rpc_port evm_port <<< "$node_config" + node_home="$BASE_DIR/$moniker" + + # 复制创世文件 + cp "$GENESIS_HOME/config/genesis.json" "$node_home/config/genesis.json" + + # 获取 node ID + NODE_ID=$($BINARY tendermint show-node-id --home "$node_home") + + if [ -n "$PERSISTENT_PEERS" ]; then + PERSISTENT_PEERS="${PERSISTENT_PEERS}," + fi + PERSISTENT_PEERS="${PERSISTENT_PEERS}${NODE_ID}@127.0.0.1:${p2p_port}" +done + +# 配置所有节点 +for node_config in "${NODES[@]}"; do + IFS=':' read -r moniker type p2p_port rpc_port evm_port <<< "$node_config" + node_home="$BASE_DIR/$moniker" + + # 配置 P2P 端口 + sed -i "s|laddr = \"tcp://0.0.0.0:26656\"|laddr = \"tcp://0.0.0.0:${p2p_port}\"|" "$node_home/config/config.toml" + sed -i "s|laddr = \"tcp://127.0.0.1:26657\"|laddr = \"tcp://127.0.0.1:${rpc_port}\"|" "$node_home/config/config.toml" + + # 配置持久对等节点 + sed -i "s|persistent_peers = \"\"|persistent_peers = \"${PERSISTENT_PEERS}\"|" "$node_home/config/config.toml" + + # Gas 补贴 + sed -i 's|minimum-gas-prices = ""|minimum-gas-prices = "0agnx"|' "$node_home/config/app.toml" + + echo " $moniker configured (P2P: $p2p_port, RPC: $rpc_port, EVM: $evm_port)" +done + +# 初始化监管只读节点 +echo "" +echo "Initializing regulatory observer node..." +REG_HOME="$BASE_DIR/genex-regulatory-1" +$BINARY init "genex-regulatory-1" --chain-id "$CHAIN_ID" --home "$REG_HOME" > /dev/null 2>&1 +cp "$GENESIS_HOME/config/genesis.json" "$REG_HOME/config/genesis.json" +sed -i "s|persistent_peers = \"\"|persistent_peers = \"${PERSISTENT_PEERS}\"|" "$REG_HOME/config/config.toml" + +echo "" +echo "============================================" +echo " Testnet Initialization Complete!" +echo "============================================" +echo "" +echo "Chain ID: $CHAIN_ID" +echo "Nodes: ${#NODES[@]} validators + 1 regulatory" +echo "Base Dir: $BASE_DIR" +echo "" +echo "To start all nodes:" +echo " for dir in $BASE_DIR/genex-*; do" +echo " $BINARY start --home \$dir &" +echo " done" +echo "" +echo "Node Endpoints:" +for node_config in "${NODES[@]}"; do + IFS=':' read -r moniker type p2p_port rpc_port evm_port <<< "$node_config" + echo " $moniker ($type):" + echo " P2P: :$p2p_port RPC: :$rpc_port EVM: :$evm_port" +done +echo "" +echo "============================================" diff --git a/blockchain/genex-chain/upgrades.go b/blockchain/genex-chain/upgrades.go new file mode 100644 index 0000000..b60cb72 --- /dev/null +++ b/blockchain/genex-chain/upgrades.go @@ -0,0 +1,31 @@ +package genexchain + +import ( + storetypes "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" +) + +// RegisterUpgradeHandlers registers upgrade handlers for chain upgrades +func (app GenexApp) RegisterUpgradeHandlers() { + // Genex Chain v1.0.0 — initial release, no upgrade handlers needed yet. + // Future upgrades will be registered here: + // + // app.UpgradeKeeper.SetUpgradeHandler( + // "v1.1.0", + // func(ctx context.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // // migration logic + // return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM) + // }, + // ) + + upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() + if err != nil { + panic(err) + } + + // Check if we need store upgrades + if upgradeInfo.Name != "" && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + storeUpgrades := storetypes.StoreUpgrades{Added: []string{}} + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + } +} diff --git a/blockchain/genex-chain/x/evm/ante/compliance_ante.go b/blockchain/genex-chain/x/evm/ante/compliance_ante.go new file mode 100644 index 0000000..40bf5a7 --- /dev/null +++ b/blockchain/genex-chain/x/evm/ante/compliance_ante.go @@ -0,0 +1,240 @@ +// Package ante — 验证节点级合规交易拦截 +// +// ComplianceAnteHandler 在验证节点打包交易前执行合规检查: +// 1. OFAC 地址拦截 — 制裁名单地址的交易直接拒绝 +// 2. Structuring 检测 — 拆分交易规避 $3,000 Travel Rule 阈值 +// 3. Travel Rule 预检查 — ≥$3,000 转移必须携带身份哈希 +// +// 参考: FinCEN Travel Rule, OFAC SDN List, FATF Recommendation 16 +package ante + +import ( + "fmt" + "math/big" + "sync" + "time" +) + +// ComplianceAnteHandler 验证节点级合规检查处理器 +type ComplianceAnteHandler struct { + mu sync.RWMutex + + // OFAC 制裁名单(地址 → 是否制裁) + ofacList map[string]bool + + // Travel Rule 记录(sender+receiver hash → 是否已记录) + travelRuleRecords map[string]bool + + // Structuring 检测:地址 → 24h 内累计转移金额 + recentTransfers map[string]*TransferWindow + + // 配置 + travelRuleThreshold *big.Int // $3,000 USDC (3000 * 1e6) + structuringWindow time.Duration +} + +// TransferWindow 滑动窗口内的转移记录 +type TransferWindow struct { + TotalAmount *big.Int + Transfers []TransferRecord +} + +// TransferRecord 单笔转移记录 +type TransferRecord struct { + Amount *big.Int + Timestamp time.Time +} + +// ComplianceResult 合规检查结果 +type ComplianceResult struct { + Allowed bool + Reason string + Suspicious bool // 标记为可疑但不拒绝 + SuspReason string // 可疑原因 +} + +// NewComplianceAnteHandler 创建合规检查处理器 +func NewComplianceAnteHandler() *ComplianceAnteHandler { + threshold := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6)) // $3,000 USDC + + return &ComplianceAnteHandler{ + ofacList: make(map[string]bool), + travelRuleRecords: make(map[string]bool), + recentTransfers: make(map[string]*TransferWindow), + travelRuleThreshold: threshold, + structuringWindow: 24 * time.Hour, + } +} + +// AnteHandle 执行合规检查(在交易打包前调用) +// +// 返回: +// - ComplianceResult.Allowed = false: 交易被拒绝,不会被打包 +// - ComplianceResult.Suspicious = true: 标记为可疑,仍可打包但上报 +func (h *ComplianceAnteHandler) AnteHandle(from, to string, value *big.Int) ComplianceResult { + h.mu.RLock() + defer h.mu.RUnlock() + + // 1. OFAC 地址拦截(链级强制) + if h.ofacList[from] { + return ComplianceResult{ + Allowed: false, + Reason: fmt.Sprintf("OFAC sanctioned address (sender: %s), transaction rejected at validator level", from), + } + } + if h.ofacList[to] { + return ComplianceResult{ + Allowed: false, + Reason: fmt.Sprintf("OFAC sanctioned address (receiver: %s), transaction rejected at validator level", to), + } + } + + result := ComplianceResult{Allowed: true} + + // 2. Structuring 检测(拆分交易规避 $3,000 阈值) + if h.isStructuringPattern(from, value) { + result.Suspicious = true + result.SuspReason = fmt.Sprintf( + "Potential structuring detected for %s: cumulative transfers approaching Travel Rule threshold", + from, + ) + } + + // 3. Travel Rule 预打包检查(≥$3,000) + if value.Cmp(h.travelRuleThreshold) >= 0 { + recordKey := fmt.Sprintf("%s:%s", from, to) + if !h.travelRuleRecords[recordKey] { + return ComplianceResult{ + Allowed: false, + Reason: fmt.Sprintf("Travel Rule: identity data required for transfers >= $3,000 (from: %s, to: %s)", from, to), + } + } + } + + return result +} + +// isStructuringPattern 检测是否存在拆分交易模式 +// 规则:24h 内同一地址的累计小额转移接近或超过 $3,000 +func (h *ComplianceAnteHandler) isStructuringPattern(from string, currentValue *big.Int) bool { + window, exists := h.recentTransfers[from] + if !exists { + return false + } + + // 清理过期记录 + now := time.Now() + var validTransfers []TransferRecord + totalAmount := new(big.Int) + for _, t := range window.Transfers { + if now.Sub(t.Timestamp) <= h.structuringWindow { + validTransfers = append(validTransfers, t) + totalAmount.Add(totalAmount, t.Amount) + } + } + + // 加上当前交易 + totalAmount.Add(totalAmount, currentValue) + + // 如果每笔都低于阈值,但累计超过阈值 → 可疑 + allBelowThreshold := true + for _, t := range validTransfers { + if t.Amount.Cmp(h.travelRuleThreshold) >= 0 { + allBelowThreshold = false + break + } + } + if currentValue.Cmp(h.travelRuleThreshold) >= 0 { + allBelowThreshold = false + } + + return allBelowThreshold && totalAmount.Cmp(h.travelRuleThreshold) >= 0 +} + +// ======================== +// OFAC 名单管理 +// ======================== + +// UpdateOFACList 更新 OFAC 制裁名单(由链下服务定期调用) +func (h *ComplianceAnteHandler) UpdateOFACList(addresses []string) { + h.mu.Lock() + defer h.mu.Unlock() + + h.ofacList = make(map[string]bool, len(addresses)) + for _, addr := range addresses { + h.ofacList[addr] = true + } +} + +// AddToOFACList 添加地址到 OFAC 名单 +func (h *ComplianceAnteHandler) AddToOFACList(address string) { + h.mu.Lock() + defer h.mu.Unlock() + h.ofacList[address] = true +} + +// RemoveFromOFACList 从 OFAC 名单移除 +func (h *ComplianceAnteHandler) RemoveFromOFACList(address string) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.ofacList, address) +} + +// IsOFACSanctioned 查询地址是否在 OFAC 名单中 +func (h *ComplianceAnteHandler) IsOFACSanctioned(address string) bool { + h.mu.RLock() + defer h.mu.RUnlock() + return h.ofacList[address] +} + +// ======================== +// Travel Rule 管理 +// ======================== + +// RecordTravelRule 记录 Travel Rule 数据 +func (h *ComplianceAnteHandler) RecordTravelRule(from, to string) { + h.mu.Lock() + defer h.mu.Unlock() + key := fmt.Sprintf("%s:%s", from, to) + h.travelRuleRecords[key] = true +} + +// HasTravelRuleRecord 查询是否已记录 Travel Rule +func (h *ComplianceAnteHandler) HasTravelRuleRecord(from, to string) bool { + h.mu.RLock() + defer h.mu.RUnlock() + key := fmt.Sprintf("%s:%s", from, to) + return h.travelRuleRecords[key] +} + +// ======================== +// 转移记录(Structuring 检测用) +// ======================== + +// RecordTransfer 记录一笔转移(用于 Structuring 检测) +func (h *ComplianceAnteHandler) RecordTransfer(from string, amount *big.Int) { + h.mu.Lock() + defer h.mu.Unlock() + + window, exists := h.recentTransfers[from] + if !exists { + window = &TransferWindow{ + TotalAmount: new(big.Int), + Transfers: make([]TransferRecord, 0), + } + h.recentTransfers[from] = window + } + + window.Transfers = append(window.Transfers, TransferRecord{ + Amount: new(big.Int).Set(amount), + Timestamp: time.Now(), + }) + window.TotalAmount.Add(window.TotalAmount, amount) +} + +// GetOFACListSize 获取当前 OFAC 名单大小 +func (h *ComplianceAnteHandler) GetOFACListSize() int { + h.mu.RLock() + defer h.mu.RUnlock() + return len(h.ofacList) +} diff --git a/blockchain/genex-chain/x/evm/ante/compliance_ante_test.go b/blockchain/genex-chain/x/evm/ante/compliance_ante_test.go new file mode 100644 index 0000000..52f741c --- /dev/null +++ b/blockchain/genex-chain/x/evm/ante/compliance_ante_test.go @@ -0,0 +1,302 @@ +package ante + +import ( + "math/big" + "testing" + "time" +) + +// ============================== +// OFAC Tests +// ============================== + +func TestOFAC_BlocksSanctionedSender(t *testing.T) { + h := NewComplianceAnteHandler() + h.AddToOFACList("0xSanctioned") + + result := h.AnteHandle("0xSanctioned", "0xNormal", big.NewInt(1000)) + if result.Allowed { + t.Fatal("should block sanctioned sender") + } + if result.Reason == "" { + t.Fatal("should provide reason") + } +} + +func TestOFAC_BlocksSanctionedReceiver(t *testing.T) { + h := NewComplianceAnteHandler() + h.AddToOFACList("0xSanctioned") + + result := h.AnteHandle("0xNormal", "0xSanctioned", big.NewInt(1000)) + if result.Allowed { + t.Fatal("should block sanctioned receiver") + } +} + +func TestOFAC_AllowsCleanAddresses(t *testing.T) { + h := NewComplianceAnteHandler() + h.AddToOFACList("0xSanctioned") + + result := h.AnteHandle("0xAlice", "0xBob", big.NewInt(1000)) + if !result.Allowed { + t.Fatalf("should allow clean addresses, got reason: %s", result.Reason) + } +} + +func TestOFAC_BulkUpdate(t *testing.T) { + h := NewComplianceAnteHandler() + + addresses := []string{"0xA", "0xB", "0xC"} + h.UpdateOFACList(addresses) + + if h.GetOFACListSize() != 3 { + t.Fatalf("expected 3, got %d", h.GetOFACListSize()) + } + + for _, addr := range addresses { + if !h.IsOFACSanctioned(addr) { + t.Fatalf("%s should be sanctioned", addr) + } + } + + if h.IsOFACSanctioned("0xClean") { + t.Fatal("0xClean should not be sanctioned") + } +} + +func TestOFAC_RemoveFromList(t *testing.T) { + h := NewComplianceAnteHandler() + h.AddToOFACList("0xAddr") + if !h.IsOFACSanctioned("0xAddr") { + t.Fatal("should be sanctioned") + } + + h.RemoveFromOFACList("0xAddr") + if h.IsOFACSanctioned("0xAddr") { + t.Fatal("should no longer be sanctioned") + } +} + +func TestOFAC_UpdateReplacesOldList(t *testing.T) { + h := NewComplianceAnteHandler() + h.AddToOFACList("0xOld") + + h.UpdateOFACList([]string{"0xNew"}) + + if h.IsOFACSanctioned("0xOld") { + t.Fatal("0xOld should be removed after full update") + } + if !h.IsOFACSanctioned("0xNew") { + t.Fatal("0xNew should be in the new list") + } +} + +// ============================== +// Travel Rule Tests +// ============================== + +func TestTravelRule_BlocksLargeTransferWithoutRecord(t *testing.T) { + h := NewComplianceAnteHandler() + + // $3,000 USDC = 3000 * 1e6 = 3,000,000,000 + amount := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6)) + + result := h.AnteHandle("0xAlice", "0xBob", amount) + if result.Allowed { + t.Fatal("should block large transfer without Travel Rule record") + } +} + +func TestTravelRule_AllowsLargeTransferWithRecord(t *testing.T) { + h := NewComplianceAnteHandler() + h.RecordTravelRule("0xAlice", "0xBob") + + amount := new(big.Int).Mul(big.NewInt(5000), big.NewInt(1e6)) + + result := h.AnteHandle("0xAlice", "0xBob", amount) + if !result.Allowed { + t.Fatalf("should allow large transfer with Travel Rule record, reason: %s", result.Reason) + } +} + +func TestTravelRule_AllowsSmallTransferWithoutRecord(t *testing.T) { + h := NewComplianceAnteHandler() + + // $2,999 — below threshold + amount := new(big.Int).Mul(big.NewInt(2999), big.NewInt(1e6)) + + result := h.AnteHandle("0xAlice", "0xBob", amount) + if !result.Allowed { + t.Fatal("should allow transfer below $3,000 without Travel Rule record") + } +} + +func TestTravelRule_ExactThreshold(t *testing.T) { + h := NewComplianceAnteHandler() + + // Exactly $3,000 — at threshold, should require record + amount := new(big.Int).Mul(big.NewInt(3000), big.NewInt(1e6)) + + result := h.AnteHandle("0xAlice", "0xBob", amount) + if result.Allowed { + t.Fatal("$3,000 exactly should require Travel Rule record") + } +} + +func TestTravelRule_HasRecord(t *testing.T) { + h := NewComplianceAnteHandler() + + if h.HasTravelRuleRecord("0xA", "0xB") { + t.Fatal("should not have record initially") + } + + h.RecordTravelRule("0xA", "0xB") + + if !h.HasTravelRuleRecord("0xA", "0xB") { + t.Fatal("should have record after recording") + } + + // Direction matters + if h.HasTravelRuleRecord("0xB", "0xA") { + t.Fatal("reverse direction should not have record") + } +} + +// ============================== +// Structuring Detection Tests +// ============================== + +func TestStructuring_NoFlagForSingleSmallTransfer(t *testing.T) { + h := NewComplianceAnteHandler() + + amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) // $500 + result := h.AnteHandle("0xAlice", "0xBob", amount) + + if result.Suspicious { + t.Fatal("single small transfer should not be flagged") + } +} + +func TestStructuring_FlagsMultipleSmallTransfersExceedingThreshold(t *testing.T) { + h := NewComplianceAnteHandler() + + // Record several small transfers totaling > $3,000 + for i := 0; i < 5; i++ { + h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6))) // $500 each + } + + // 6th transfer: cumulative = $3,000 + amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) + result := h.AnteHandle("0xAlice", "0xBob", amount) + + if !result.Suspicious { + t.Fatal("cumulative small transfers exceeding $3,000 should be flagged as suspicious") + } + // But still allowed (not blocked) + if !result.Allowed { + t.Fatal("structuring should flag but not block") + } +} + +func TestStructuring_DoesNotFlagIfSingleLargeTransfer(t *testing.T) { + h := NewComplianceAnteHandler() + + // Record one large transfer > threshold + h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(4000), big.NewInt(1e6))) + + // Another small transfer + amount := new(big.Int).Mul(big.NewInt(100), big.NewInt(1e6)) + result := h.AnteHandle("0xAlice", "0xBob", amount) + + // Not structuring because previous transfers include one >= threshold + if result.Suspicious { + t.Fatal("should not flag as structuring if previous transfers include large ones") + } +} + +func TestStructuring_SlidingWindowExpiry(t *testing.T) { + h := NewComplianceAnteHandler() + // Override window to 1ms for testing + h.structuringWindow = 1 * time.Millisecond + + h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(2000), big.NewInt(1e6))) + + // Wait for window to expire + time.Sleep(5 * time.Millisecond) + + amount := new(big.Int).Mul(big.NewInt(1500), big.NewInt(1e6)) + result := h.AnteHandle("0xAlice", "0xBob", amount) + + if result.Suspicious { + t.Fatal("expired window records should not contribute to structuring detection") + } +} + +// ============================== +// Combined Scenario Tests +// ============================== + +func TestCombined_OFACTakesPrecedenceOverTravelRule(t *testing.T) { + h := NewComplianceAnteHandler() + h.AddToOFACList("0xBad") + h.RecordTravelRule("0xBad", "0xGood") + + amount := new(big.Int).Mul(big.NewInt(5000), big.NewInt(1e6)) + result := h.AnteHandle("0xBad", "0xGood", amount) + + if result.Allowed { + t.Fatal("OFAC should block even if Travel Rule record exists") + } +} + +func TestCombined_StructuringPlusTravelRule(t *testing.T) { + h := NewComplianceAnteHandler() + + // Record small transfers approaching threshold + for i := 0; i < 3; i++ { + h.RecordTransfer("0xAlice", new(big.Int).Mul(big.NewInt(900), big.NewInt(1e6))) + } + + // Small transfer that pushes cumulative over threshold + // But this single transfer is below threshold, so no Travel Rule needed + amount := new(big.Int).Mul(big.NewInt(500), big.NewInt(1e6)) + result := h.AnteHandle("0xAlice", "0xBob", amount) + + if !result.Allowed { + t.Fatal("below-threshold transfer should still be allowed") + } + if !result.Suspicious { + t.Fatal("should be flagged as suspicious structuring") + } +} + +func TestConcurrency_SafeAccess(t *testing.T) { + h := NewComplianceAnteHandler() + + done := make(chan struct{}) + + // Writer goroutine + go func() { + for i := 0; i < 100; i++ { + h.AddToOFACList("0xAddr") + h.RemoveFromOFACList("0xAddr") + h.RecordTravelRule("0xA", "0xB") + h.RecordTransfer("0xC", big.NewInt(1000)) + } + done <- struct{}{} + }() + + // Reader goroutine + go func() { + for i := 0; i < 100; i++ { + h.AnteHandle("0xAlice", "0xBob", big.NewInt(1000)) + h.IsOFACSanctioned("0xAddr") + h.HasTravelRuleRecord("0xA", "0xB") + h.GetOFACListSize() + } + done <- struct{}{} + }() + + <-done + <-done +} diff --git a/blockchain/genex-chain/x/evm/keeper/gas.go b/blockchain/genex-chain/x/evm/keeper/gas.go new file mode 100644 index 0000000..c236a93 --- /dev/null +++ b/blockchain/genex-chain/x/evm/keeper/gas.go @@ -0,0 +1,117 @@ +// Package keeper — EVM Gas 费覆盖逻辑 +// +// Genex Chain 的 Gas 策略: +// - 当前阶段:平台全额补贴,min_gas_price = 0 +// - Gas 参数可通过 Governance 合约动态调整(无需硬分叉) +// - 后期可开启 EIP-1559 费用市场或固定 Gas 费 +package keeper + +import ( + "math/big" +) + +// GasConfig 定义 Gas 费配置 +type GasConfig struct { + // MinGasPrice 最低 Gas 价格(当前为 0,平台补贴) + MinGasPrice *big.Int `json:"min_gas_price"` + // MaxGasPerBlock 每区块最大 Gas + MaxGasPerBlock uint64 `json:"max_gas_per_block"` + // EnableEIP1559 是否启用 EIP-1559 动态费用 + EnableEIP1559 bool `json:"enable_eip1559"` + // BaseFeeChangeDenominator EIP-1559 基础费变化分母 + BaseFeeChangeDenominator uint64 `json:"base_fee_change_denominator"` + // ElasticityMultiplier 弹性乘数 + ElasticityMultiplier uint64 `json:"elasticity_multiplier"` +} + +// DefaultGasConfig 返回默认 Gas 配置(平台全额补贴) +func DefaultGasConfig() GasConfig { + return GasConfig{ + MinGasPrice: big.NewInt(0), // 免费 Gas + MaxGasPerBlock: 100_000_000, // 1 亿 Gas 上限 + EnableEIP1559: false, // 当前不启用 EIP-1559 + BaseFeeChangeDenominator: 8, + ElasticityMultiplier: 2, + } +} + +// EVMKeeper 管理 EVM 模块状态,包含 Gas 费逻辑 +type EVMKeeper struct { + gasConfig GasConfig +} + +// NewEVMKeeper 创建 EVMKeeper 实例 +func NewEVMKeeper(config GasConfig) *EVMKeeper { + return &EVMKeeper{ + gasConfig: config, + } +} + +// GetBaseFee 返回当前基础 Gas 费 +// 当前阶段:返回 0(平台全额补贴,用户无需支付 Gas) +// 后期可通过 Governance 合约调整 MinGasPrice 参数开启收费 +func (k *EVMKeeper) GetBaseFee() *big.Int { + if k.gasConfig.MinGasPrice == nil || k.gasConfig.MinGasPrice.Sign() == 0 { + return big.NewInt(0) // 免费 Gas + } + + if k.gasConfig.EnableEIP1559 { + return k.calculateEIP1559BaseFee() + } + + return new(big.Int).Set(k.gasConfig.MinGasPrice) +} + +// GetMinGasPrice 获取最低 Gas 价格 +func (k *EVMKeeper) GetMinGasPrice() *big.Int { + if k.gasConfig.MinGasPrice == nil { + return big.NewInt(0) + } + return new(big.Int).Set(k.gasConfig.MinGasPrice) +} + +// SetMinGasPrice 设置最低 Gas 价格(由 Governance 合约通过提案调用) +func (k *EVMKeeper) SetMinGasPrice(price *big.Int) { + k.gasConfig.MinGasPrice = new(big.Int).Set(price) +} + +// IsGasFree 检查当前是否为免费 Gas 模式 +func (k *EVMKeeper) IsGasFree() bool { + return k.gasConfig.MinGasPrice == nil || k.gasConfig.MinGasPrice.Sign() == 0 +} + +// GetMaxGasPerBlock 获取每区块最大 Gas 限制 +func (k *EVMKeeper) GetMaxGasPerBlock() uint64 { + return k.gasConfig.MaxGasPerBlock +} + +// SetMaxGasPerBlock 设置每区块最大 Gas(由 Governance 调整) +func (k *EVMKeeper) SetMaxGasPerBlock(maxGas uint64) { + k.gasConfig.MaxGasPerBlock = maxGas +} + +// EnableEIP1559 启用/禁用 EIP-1559(由 Governance 控制) +func (k *EVMKeeper) EnableEIP1559(enable bool) { + k.gasConfig.EnableEIP1559 = enable +} + +// calculateEIP1559BaseFee 计算 EIP-1559 动态基础费 +// 预留接口,当前阶段不启用 +func (k *EVMKeeper) calculateEIP1559BaseFee() *big.Int { + // EIP-1559 基础费计算逻辑 + // 生产环境中应根据上一区块的 Gas 使用率动态调整 + // 当前返回最低价格 + return new(big.Int).Set(k.gasConfig.MinGasPrice) +} + +// GetGasConfig 获取当前 Gas 配置(用于 API 查询) +func (k *EVMKeeper) GetGasConfig() GasConfig { + return k.gasConfig +} + +// EstimateGasCost 估算交易 Gas 费用 +// 当前阶段始终返回 0(平台补贴) +func (k *EVMKeeper) EstimateGasCost(gasUsed uint64) *big.Int { + baseFee := k.GetBaseFee() + return new(big.Int).Mul(baseFee, new(big.Int).SetUint64(gasUsed)) +} diff --git a/blockchain/genex-chain/x/evm/keeper/gas_test.go b/blockchain/genex-chain/x/evm/keeper/gas_test.go new file mode 100644 index 0000000..fff5475 --- /dev/null +++ b/blockchain/genex-chain/x/evm/keeper/gas_test.go @@ -0,0 +1,157 @@ +package keeper + +import ( + "math/big" + "testing" +) + +func TestDefaultGasConfig_IsGasFree(t *testing.T) { + config := DefaultGasConfig() + keeper := NewEVMKeeper(config) + + if !keeper.IsGasFree() { + t.Fatal("default config should be gas-free (platform subsidy)") + } +} + +func TestGetBaseFee_ReturnsZeroWhenFree(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + fee := keeper.GetBaseFee() + + if fee.Sign() != 0 { + t.Fatalf("base fee should be 0, got %s", fee.String()) + } +} + +func TestGetBaseFee_ReturnsMinGasPriceWhenSet(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + keeper.SetMinGasPrice(big.NewInt(1000)) + + fee := keeper.GetBaseFee() + if fee.Cmp(big.NewInt(1000)) != 0 { + t.Fatalf("expected 1000, got %s", fee.String()) + } +} + +func TestGetBaseFee_EIP1559Mode(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + keeper.SetMinGasPrice(big.NewInt(500)) + keeper.EnableEIP1559(true) + + fee := keeper.GetBaseFee() + // In current implementation, EIP-1559 returns min gas price + if fee.Cmp(big.NewInt(500)) != 0 { + t.Fatalf("EIP-1559 base fee should be min gas price, got %s", fee.String()) + } +} + +func TestSetMinGasPrice(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + + keeper.SetMinGasPrice(big.NewInt(2000)) + + price := keeper.GetMinGasPrice() + if price.Cmp(big.NewInt(2000)) != 0 { + t.Fatalf("expected 2000, got %s", price.String()) + } + if keeper.IsGasFree() { + t.Fatal("should not be gas-free after setting min gas price") + } +} + +func TestSetMinGasPriceBackToZero(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + keeper.SetMinGasPrice(big.NewInt(1000)) + keeper.SetMinGasPrice(big.NewInt(0)) + + if !keeper.IsGasFree() { + t.Fatal("should be gas-free after setting price back to 0") + } +} + +func TestGetMaxGasPerBlock(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + + maxGas := keeper.GetMaxGasPerBlock() + if maxGas != 100_000_000 { + t.Fatalf("expected 100M, got %d", maxGas) + } +} + +func TestSetMaxGasPerBlock(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + keeper.SetMaxGasPerBlock(200_000_000) + + if keeper.GetMaxGasPerBlock() != 200_000_000 { + t.Fatalf("expected 200M, got %d", keeper.GetMaxGasPerBlock()) + } +} + +func TestEstimateGasCost_FreeMode(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + + cost := keeper.EstimateGasCost(21000) // standard ETH transfer + if cost.Sign() != 0 { + t.Fatalf("gas cost should be 0 in free mode, got %s", cost.String()) + } +} + +func TestEstimateGasCost_PaidMode(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + keeper.SetMinGasPrice(big.NewInt(10)) // 10 wei + + cost := keeper.EstimateGasCost(21000) + expected := big.NewInt(210000) // 10 * 21000 + if cost.Cmp(expected) != 0 { + t.Fatalf("expected %s, got %s", expected.String(), cost.String()) + } +} + +func TestGetGasConfig(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + + config := keeper.GetGasConfig() + if config.MaxGasPerBlock != 100_000_000 { + t.Fatal("config MaxGasPerBlock mismatch") + } + if config.EnableEIP1559 { + t.Fatal("EIP-1559 should be disabled by default") + } + if config.BaseFeeChangeDenominator != 8 { + t.Fatal("BaseFeeChangeDenominator mismatch") + } + if config.ElasticityMultiplier != 2 { + t.Fatal("ElasticityMultiplier mismatch") + } +} + +func TestEnableEIP1559(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + + keeper.EnableEIP1559(true) + config := keeper.GetGasConfig() + if !config.EnableEIP1559 { + t.Fatal("EIP-1559 should be enabled") + } + + keeper.EnableEIP1559(false) + config = keeper.GetGasConfig() + if config.EnableEIP1559 { + t.Fatal("EIP-1559 should be disabled") + } +} + +func TestMinGasPrice_IsolatedCopy(t *testing.T) { + keeper := NewEVMKeeper(DefaultGasConfig()) + keeper.SetMinGasPrice(big.NewInt(1000)) + + // Get price and modify the returned value + price := keeper.GetMinGasPrice() + price.SetInt64(9999) + + // Original should not be affected + original := keeper.GetMinGasPrice() + if original.Cmp(big.NewInt(1000)) != 0 { + t.Fatal("GetMinGasPrice should return a copy, not a reference") + } +} diff --git a/blockchain/genex-contracts/.gitignore b/blockchain/genex-contracts/.gitignore new file mode 100644 index 0000000..828fc98 --- /dev/null +++ b/blockchain/genex-contracts/.gitignore @@ -0,0 +1,10 @@ +# Dependencies (restore with: forge install) +lib/ + +# Build artifacts +out/ +cache/ +broadcast/ + +# Environment +.env diff --git a/blockchain/genex-contracts/.gitmodules b/blockchain/genex-contracts/.gitmodules new file mode 100644 index 0000000..8f34b8e --- /dev/null +++ b/blockchain/genex-contracts/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/blockchain/genex-contracts/Dockerfile b/blockchain/genex-contracts/Dockerfile new file mode 100644 index 0000000..21e3bbc --- /dev/null +++ b/blockchain/genex-contracts/Dockerfile @@ -0,0 +1,71 @@ +# ============================================================ +# Genex Contracts — Foundry 部署镜像 +# +# 用于通过 Foundry 将合约部署到 Genex Chain +# +# 构建: docker build -t genex-contracts:latest . +# 部署: docker run -e RPC_URL=http://localhost:8545 \ +# -e DEPLOYER_PRIVATE_KEY=0x... \ +# -e USDC_ADDRESS=0x... \ +# genex-contracts:latest +# ============================================================ + +FROM ghcr.io/foundry-rs/foundry:latest + +WORKDIR /contracts + +# 复制项目文件 +COPY foundry.toml remappings.txt ./ +COPY lib/ ./lib/ +COPY src/ ./src/ +COPY script/ ./script/ +COPY test/ ./test/ + +# 编译合约 +RUN forge build + +# 部署脚本 +COPY <<'DEPLOY_SCRIPT' /deploy.sh +#!/bin/bash +set -euo pipefail + +echo "============================================" +echo " Genex Contracts — Deployment" +echo "============================================" + +RPC_URL="${RPC_URL:-http://localhost:8545}" +echo "RPC: $RPC_URL" + +# 等待链节点就绪 +echo "Waiting for chain node..." +for i in $(seq 1 30); do + if cast chain-id --rpc-url "$RPC_URL" 2>/dev/null; then + echo "Chain node is ready!" + break + fi + echo " Attempt $i/30..." + sleep 2 +done + +# 运行测试确认合约正确 +echo "" +echo "Running contract tests..." +forge test --no-match-test "testFuzz" -v + +# 部署 +echo "" +echo "Deploying contracts..." +forge script script/Deploy.s.sol \ + --rpc-url "$RPC_URL" \ + --broadcast \ + -vvv + +echo "" +echo "============================================" +echo " Deployment Complete!" +echo "============================================" +DEPLOY_SCRIPT + +RUN chmod +x /deploy.sh + +ENTRYPOINT ["/deploy.sh"] diff --git a/blockchain/genex-contracts/foundry.lock b/blockchain/genex-contracts/foundry.lock new file mode 100644 index 0000000..9bb7f6c --- /dev/null +++ b/blockchain/genex-contracts/foundry.lock @@ -0,0 +1,20 @@ +{ + "lib\\forge-std": { + "tag": { + "name": "v1.14.0", + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" + } + }, + "lib\\openzeppelin-contracts": { + "tag": { + "name": "v4.9.6", + "rev": "dc44c9f1a4c3b10af99492eed84f83ed244203f6" + } + }, + "lib\\openzeppelin-contracts-upgradeable": { + "tag": { + "name": "v4.9.6", + "rev": "2d081f24cac1a867f6f73d512f2022e1fa987854" + } + } +} \ No newline at end of file diff --git a/blockchain/genex-contracts/foundry.toml b/blockchain/genex-contracts/foundry.toml new file mode 100644 index 0000000..2511bb0 --- /dev/null +++ b/blockchain/genex-contracts/foundry.toml @@ -0,0 +1,24 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.20" +optimizer = true +optimizer_runs = 200 +evm_version = "shanghai" +via_ir = true +ffi = false +fuzz = { runs = 256, max_test_rejects = 65536 } + +[profile.default.fmt] +line_length = 120 +tab_width = 4 +bracket_spacing = true + +[rpc_endpoints] +local = "http://127.0.0.1:8545" +genex_testnet = "https://testnet-rpc.gogenex.com" +genex_mainnet = "https://rpc.gogenex.com" + +[etherscan] +genex = { key = "${GENEX_EXPLORER_KEY}", chain = 8888 } diff --git a/blockchain/genex-contracts/remappings.txt b/blockchain/genex-contracts/remappings.txt new file mode 100644 index 0000000..1c16eeb --- /dev/null +++ b/blockchain/genex-contracts/remappings.txt @@ -0,0 +1,2 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ diff --git a/blockchain/genex-contracts/script/Deploy.s.sol b/blockchain/genex-contracts/script/Deploy.s.sol new file mode 100644 index 0000000..abcad6d --- /dev/null +++ b/blockchain/genex-contracts/script/Deploy.s.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +import "../src/Coupon.sol"; +import "../src/CouponFactory.sol"; +import "../src/Settlement.sol"; +import "../src/Redemption.sol"; +import "../src/Compliance.sol"; +import "../src/Treasury.sol"; +import "../src/Governance.sol"; +import "../src/ExchangeRateOracle.sol"; +import "../src/CouponBackedSecurity.sol"; + +/// @title Deploy — Genex 合约系统完整部署脚本 +/// @notice 部署所有合约(Transparent Proxy 模式) + 角色授权 + 初始化 +contract Deploy is Script { + // 部署后的合约地址 + ProxyAdmin public proxyAdmin; + address public coupon; + address public couponFactory; + address public settlement; + address public redemption; + address public compliance; + address public treasury; + address public governance; + address public oracle; + address public cbs; + + function run() external { + uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.addr(deployerKey); + address usdc = vm.envAddress("USDC_ADDRESS"); + address platformFeeCollector = vm.envOr("FEE_COLLECTOR", deployer); + + // 多签成员地址 + address[] memory multisigMembers = new address[](5); + multisigMembers[0] = vm.envOr("MULTISIG_1", deployer); + multisigMembers[1] = vm.envOr("MULTISIG_2", deployer); + multisigMembers[2] = vm.envOr("MULTISIG_3", deployer); + multisigMembers[3] = vm.envOr("MULTISIG_4", deployer); + multisigMembers[4] = vm.envOr("MULTISIG_5", deployer); + + vm.startBroadcast(deployerKey); + + // ========================================== + // 1. 部署 ProxyAdmin + // ========================================== + proxyAdmin = new ProxyAdmin(); + console.log("ProxyAdmin:", address(proxyAdmin)); + + // ========================================== + // 2. 部署 Compliance(最先,其他合约依赖它) + // ========================================== + Compliance complianceImpl = new Compliance(); + compliance = _deployProxy( + address(complianceImpl), + abi.encodeCall(Compliance.initialize, (deployer)) + ); + console.log("Compliance:", compliance); + + // ========================================== + // 3. 部署 Coupon + // ========================================== + Coupon couponImpl = new Coupon(); + coupon = _deployProxy( + address(couponImpl), + abi.encodeCall(Coupon.initialize, ("Genex Coupon", "GXC", deployer)) + ); + console.log("Coupon:", coupon); + + // ========================================== + // 4. 部署 CouponFactory + // ========================================== + CouponFactory factoryImpl = new CouponFactory(); + couponFactory = _deployProxy( + address(factoryImpl), + abi.encodeCall(CouponFactory.initialize, (coupon, compliance, deployer)) + ); + console.log("CouponFactory:", couponFactory); + + // ========================================== + // 5. 部署 Settlement + // ========================================== + Settlement settlementImpl = new Settlement(); + settlement = _deployProxy( + address(settlementImpl), + abi.encodeCall(Settlement.initialize, (coupon, compliance, usdc, deployer)) + ); + console.log("Settlement:", settlement); + + // ========================================== + // 6. 部署 Redemption + // ========================================== + Redemption redemptionImpl = new Redemption(); + redemption = _deployProxy( + address(redemptionImpl), + abi.encodeCall(Redemption.initialize, (coupon, compliance, deployer)) + ); + console.log("Redemption:", redemption); + + // ========================================== + // 7. 部署 Treasury + // ========================================== + Treasury treasuryImpl = new Treasury(); + treasury = _deployProxy( + address(treasuryImpl), + abi.encodeCall(Treasury.initialize, (usdc, platformFeeCollector, deployer)) + ); + console.log("Treasury:", treasury); + + // ========================================== + // 8. 部署 Governance + // ========================================== + Governance governanceImpl = new Governance(); + governance = _deployProxy( + address(governanceImpl), + abi.encodeCall(Governance.initialize, (multisigMembers, deployer)) + ); + console.log("Governance:", governance); + + // ========================================== + // 9. 部署 ExchangeRateOracle + // ========================================== + ExchangeRateOracle oracleImpl = new ExchangeRateOracle(); + oracle = _deployProxy( + address(oracleImpl), + abi.encodeCall(ExchangeRateOracle.initialize, (900, deployer)) + ); + console.log("ExchangeRateOracle:", oracle); + + // ========================================== + // 10. 部署 CouponBackedSecurity + // ========================================== + CouponBackedSecurity cbsImpl = new CouponBackedSecurity(); + cbs = _deployProxy( + address(cbsImpl), + abi.encodeCall(CouponBackedSecurity.initialize, (coupon, compliance, usdc, deployer)) + ); + console.log("CouponBackedSecurity:", cbs); + + // ========================================== + // 11. 角色授权 + // ========================================== + _grantRoles(deployer); + + vm.stopBroadcast(); + + // 输出部署摘要 + console.log("\n========== Deployment Summary =========="); + console.log("ProxyAdmin: ", address(proxyAdmin)); + console.log("Compliance: ", compliance); + console.log("Coupon: ", coupon); + console.log("CouponFactory: ", couponFactory); + console.log("Settlement: ", settlement); + console.log("Redemption: ", redemption); + console.log("Treasury: ", treasury); + console.log("Governance: ", governance); + console.log("ExchangeRateOracle: ", oracle); + console.log("CouponBackedSecurity:", cbs); + console.log("========================================="); + } + + /// @dev 部署 Transparent Proxy + function _deployProxy(address implementation, bytes memory initData) internal returns (address) { + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + implementation, + address(proxyAdmin), + initData + ); + return address(proxy); + } + + /// @dev 授予各合约之间的角色 + function _grantRoles(address deployer) internal { + // CouponFactory 需要 Coupon 的 FACTORY_ROLE + Coupon(coupon).grantRole( + keccak256("FACTORY_ROLE"), + couponFactory + ); + + // Settlement 需要 Coupon 的 SETTLER_ROLE + Coupon(coupon).grantRole( + keccak256("SETTLER_ROLE"), + settlement + ); + + // Redemption 需要通过 Coupon.burn 权限(owner 或 approved) + // — Redemption 合约中 burn 通过 ownerOf 检查,无需额外角色 + + // Governance 的 MULTISIG_ROLE 在 initialize 中已设置 + + console.log("Roles granted successfully"); + } +} diff --git a/blockchain/genex-contracts/src/Compliance.sol b/blockchain/genex-contracts/src/Compliance.sol new file mode 100644 index 0000000..80aefbb --- /dev/null +++ b/blockchain/genex-contracts/src/Compliance.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "./interfaces/ICoupon.sol"; + +/// @title Compliance — 合规合约 +/// @notice OFAC 黑名单筛查、Travel Rule、KYC 差异化检查、账户冻结 +/// @dev 使用 Transparent Proxy 部署 +contract Compliance is Initializable, AccessControlUpgradeable { + bytes32 public constant COMPLIANCE_OFFICER_ROLE = keccak256("COMPLIANCE_OFFICER_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + bytes32 public constant KYC_PROVIDER_ROLE = keccak256("KYC_PROVIDER_ROLE"); + + // OFAC 黑名单 + mapping(address => bool) private _blacklist; + // 冻结账户(紧急冻结,需 Governance 多签) + mapping(address => bool) private _frozen; + // KYC 等级映射: 0=L0(未验证), 1=L1(基本), 2=L2(增强), 3=L3(机构) + mapping(address => uint8) private _kycLevels; + // Travel Rule 记录 + mapping(address => mapping(address => bool)) private _travelRuleRecorded; + // Travel Rule 详情 + mapping(bytes32 => TravelRuleRecord) private _travelRuleRecords; + + struct TravelRuleRecord { + address sender; + address receiver; + bytes32 senderInfoHash; + bytes32 receiverInfoHash; + uint256 recordedAt; + } + + // Travel Rule 阈值(美元,USDC 精度 6 位) + uint256 public constant TRAVEL_RULE_THRESHOLD = 3000e6; // $3,000 + // 大额交易阈值 + uint256 public constant LARGE_TRADE_THRESHOLD = 10000e6; // $10,000 + + // --- Events --- + event AddressBlacklisted(address indexed account, string reason); + event AddressRemovedFromBlacklist(address indexed account); + event AccountFrozen(address indexed account); + event AccountUnfrozen(address indexed account); + event KycLevelUpdated(address indexed account, uint8 oldLevel, uint8 newLevel); + event TravelRuleRecorded( + address indexed sender, + address indexed receiver, + bytes32 senderInfoHash, + bytes32 receiverInfoHash + ); + + function initialize(address admin) external initializer { + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(COMPLIANCE_OFFICER_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + _grantRole(KYC_PROVIDER_ROLE, admin); + } + + // ======================== + // OFAC 黑名单 + // ======================== + + /// @notice OFAC 筛查 + function isBlacklisted(address account) external view returns (bool) { + return _blacklist[account]; + } + + /// @notice 添加 OFAC 黑名单 + function addToBlacklist(address account, string calldata reason) + external + onlyRole(COMPLIANCE_OFFICER_ROLE) + { + _blacklist[account] = true; + emit AddressBlacklisted(account, reason); + } + + /// @notice 从黑名单移除 + function removeFromBlacklist(address account) external onlyRole(GOVERNANCE_ROLE) { + _blacklist[account] = false; + emit AddressRemovedFromBlacklist(account); + } + + // ======================== + // 账户冻结 + // ======================== + + /// @notice 查询是否冻结 + function isFrozen(address account) external view returns (bool) { + return _frozen[account]; + } + + /// @notice 紧急冻结(需 Governance 多签) + function freezeAccount(address account) external onlyRole(GOVERNANCE_ROLE) { + _frozen[account] = true; + emit AccountFrozen(account); + } + + /// @notice 解除冻结 + function unfreezeAccount(address account) external onlyRole(GOVERNANCE_ROLE) { + _frozen[account] = false; + emit AccountUnfrozen(account); + } + + // ======================== + // KYC 等级管理 + // ======================== + + /// @notice 获取 KYC 等级 + function getKycLevel(address account) external view returns (uint8) { + return _kycLevels[account]; + } + + /// @notice 设置 KYC 等级 + function setKycLevel(address account, uint8 level) external onlyRole(KYC_PROVIDER_ROLE) { + require(level <= 3, "Compliance: invalid KYC level"); + uint8 oldLevel = _kycLevels[account]; + _kycLevels[account] = level; + emit KycLevelUpdated(account, oldLevel, level); + } + + /// @notice 批量设置 KYC 等级 + function batchSetKycLevel(address[] calldata accounts, uint8[] calldata levels) + external + onlyRole(KYC_PROVIDER_ROLE) + { + require(accounts.length == levels.length, "Compliance: length mismatch"); + for (uint256 i = 0; i < accounts.length; i++) { + require(levels[i] <= 3, "Compliance: invalid KYC level"); + uint8 oldLevel = _kycLevels[accounts[i]]; + _kycLevels[accounts[i]] = levels[i]; + emit KycLevelUpdated(accounts[i], oldLevel, levels[i]); + } + } + + /// @notice 要求最低 KYC 等级(revert if不满足) + function requireKycLevel(address account, uint8 requiredLevel) external view { + require(_kycLevels[account] >= requiredLevel, "Compliance: insufficient KYC level"); + } + + // ======================== + // Travel Rule + // ======================== + + /// @notice 记录 Travel Rule 数据(≥$3,000 强制) + function recordTravelRule( + address sender, + address receiver, + bytes32 senderInfoHash, + bytes32 receiverInfoHash + ) external onlyRole(COMPLIANCE_OFFICER_ROLE) { + require(sender != address(0) && receiver != address(0), "Compliance: zero address"); + require(senderInfoHash != bytes32(0) && receiverInfoHash != bytes32(0), "Compliance: empty hash"); + + _travelRuleRecorded[sender][receiver] = true; + + bytes32 recordId = keccak256(abi.encodePacked(sender, receiver, block.timestamp)); + _travelRuleRecords[recordId] = TravelRuleRecord({ + sender: sender, + receiver: receiver, + senderInfoHash: senderInfoHash, + receiverInfoHash: receiverInfoHash, + recordedAt: block.timestamp + }); + + emit TravelRuleRecorded(sender, receiver, senderInfoHash, receiverInfoHash); + } + + /// @notice 查询 Travel Rule 是否已记录 + function hasTravelRuleRecord(address sender, address receiver) external view returns (bool) { + return _travelRuleRecorded[sender][receiver]; + } + + // ======================== + // 综合合规检查 + // ======================== + + /// @notice 交易前合规检查(Settlement 调用) + function preTradeCheck( + address buyer, + address seller, + uint256 amount, + ICoupon.CouponType couponType + ) external view { + // OFAC 黑名单检查 + require(!_blacklist[buyer], "Compliance: buyer blacklisted"); + require(!_blacklist[seller], "Compliance: seller blacklisted"); + + // 冻结检查 + require(!_frozen[buyer], "Compliance: buyer frozen"); + require(!_frozen[seller], "Compliance: seller frozen"); + + // Utility Track: 双方至少 KYC L1 + if (couponType == ICoupon.CouponType.Utility) { + require(_kycLevels[buyer] >= 1, "Compliance: buyer needs KYC L1 for Utility"); + require(_kycLevels[seller] >= 1, "Compliance: seller needs KYC L1 for Utility"); + } + // Securities Track: 双方至少 KYC L2 + else { + require(_kycLevels[buyer] >= 2, "Compliance: buyer needs KYC L2 for Security"); + require(_kycLevels[seller] >= 2, "Compliance: seller needs KYC L2 for Security"); + } + + // 大额交易: KYC L2+ + if (amount >= LARGE_TRADE_THRESHOLD) { + require(_kycLevels[buyer] >= 2, "Compliance: buyer needs KYC L2 for large trade"); + require(_kycLevels[seller] >= 2, "Compliance: seller needs KYC L2 for large trade"); + } + } + + /// @notice P2P 转移合规路由 + function p2pComplianceCheck( + address sender, + address receiver, + uint256 amount + ) external view { + require(!_blacklist[sender], "Compliance: sender blacklisted"); + require(!_blacklist[receiver], "Compliance: receiver blacklisted"); + require(!_frozen[sender], "Compliance: sender frozen"); + require(!_frozen[receiver], "Compliance: receiver frozen"); + + // ≥$3,000 强制 Travel Rule + if (amount >= TRAVEL_RULE_THRESHOLD) { + require(_kycLevels[sender] >= 2, "Compliance: sender needs KYC L2 for Travel Rule"); + require(_kycLevels[receiver] >= 2, "Compliance: receiver needs KYC L2 for Travel Rule"); + require(_travelRuleRecorded[sender][receiver], "Compliance: Travel Rule data required"); + } + } +} diff --git a/blockchain/genex-contracts/src/Coupon.sol b/blockchain/genex-contracts/src/Coupon.sol new file mode 100644 index 0000000..4b3ecd8 --- /dev/null +++ b/blockchain/genex-contracts/src/Coupon.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./interfaces/ICoupon.sol"; + +/// @title Coupon — 券 NFT 合约 +/// @notice ERC-721 实现,支持不可转让限制、转售计数、批量操作 +/// @dev 使用 Transparent Proxy 部署,券类型铸造后不可修改(安全红线) +contract Coupon is Initializable, ERC721Upgradeable, AccessControlUpgradeable { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE"); + bytes32 public constant FACTORY_ROLE = keccak256("FACTORY_ROLE"); + + uint256 private _nextTokenId; + + // tokenId → 券配置(铸造后 couponType 不可修改 — 安全红线) + mapping(uint256 => ICoupon.CouponConfig) private _configs; + // tokenId → 面值 + mapping(uint256 => uint256) private _faceValues; + // tokenId → 转售次数(安全红线:不可被升级绕过) + mapping(uint256 => uint256) private _resaleCount; + + // --- Events --- + event CouponMinted(uint256 indexed tokenId, address indexed issuer, uint256 faceValue, ICoupon.CouponType couponType); + event CouponBurned(uint256 indexed tokenId, address indexed burner); + event ResaleCountIncremented(uint256 indexed tokenId, uint256 newCount); + + function initialize(string memory name, string memory symbol, address admin) external initializer { + __ERC721_init(name, symbol); + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(MINTER_ROLE, admin); + _nextTokenId = 1; + } + + /// @notice 铸造单个券 NFT + function mint( + address to, + uint256 faceValue, + ICoupon.CouponConfig calldata config + ) external onlyRole(FACTORY_ROLE) returns (uint256 tokenId) { + tokenId = _nextTokenId++; + _safeMint(to, tokenId); + _configs[tokenId] = config; + _faceValues[tokenId] = faceValue; + _resaleCount[tokenId] = 0; + + emit CouponMinted(tokenId, config.issuer, faceValue, config.couponType); + } + + /// @notice 销毁券(兑付时调用) + function burn(uint256 tokenId) external { + require( + _isApprovedOrOwner(msg.sender, tokenId) || hasRole(SETTLER_ROLE, msg.sender), + "Coupon: not authorized to burn" + ); + address owner = ownerOf(tokenId); + _burn(tokenId); + emit CouponBurned(tokenId, owner); + } + + /// @notice 获取券配置 + function getConfig(uint256 tokenId) external view returns (ICoupon.CouponConfig memory) { + require(_exists(tokenId), "Coupon: nonexistent token"); + return _configs[tokenId]; + } + + /// @notice 获取面值 + function getFaceValue(uint256 tokenId) external view returns (uint256) { + require(_exists(tokenId), "Coupon: nonexistent token"); + return _faceValues[tokenId]; + } + + /// @notice 获取转售次数 + function getResaleCount(uint256 tokenId) external view returns (uint256) { + return _resaleCount[tokenId]; + } + + /// @notice 增加转售次数(仅 Settlement 合约可调用) + function incrementResaleCount(uint256 tokenId) external onlyRole(SETTLER_ROLE) { + _resaleCount[tokenId]++; + emit ResaleCountIncremented(tokenId, _resaleCount[tokenId]); + } + + /// @notice 批量转移 + function batchTransfer( + address from, + address to, + uint256[] calldata tokenIds + ) external { + for (uint256 i = 0; i < tokenIds.length; i++) { + safeTransferFrom(from, to, tokenIds[i]); + } + } + + /// @notice 重写 transfer 钩子 — 不可转让券直接 revert + /// @dev 铸造(from=0)和销毁(to=0)不受限 + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override { + super._beforeTokenTransfer(from, to, tokenId, batchSize); + + // 铸造和销毁不受限 + if (from == address(0) || to == address(0)) return; + + ICoupon.CouponConfig memory config = _configs[tokenId]; + + // 不可转让券:revert + require(config.transferable, "Coupon: non-transferable"); + + // 转售次数检查(安全红线) + require( + _resaleCount[tokenId] < config.maxResaleCount, + "Coupon: max resale count exceeded" + ); + } + + /// @dev 内部辅助:检查授权(override OZ v4) + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view override returns (bool) { + return super._isApprovedOrOwner(spender, tokenId); + } + + // --- ERC165 --- + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC721Upgradeable, AccessControlUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/blockchain/genex-contracts/src/CouponBackedSecurity.sol b/blockchain/genex-contracts/src/CouponBackedSecurity.sol new file mode 100644 index 0000000..5545f1e --- /dev/null +++ b/blockchain/genex-contracts/src/CouponBackedSecurity.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import "./interfaces/ICoupon.sol"; +import "./interfaces/ICompliance.sol"; + +/// @title CouponBackedSecurity — CBS 资产证券化合约 +/// @notice 券收益流打包为资产支持证券,Securities Track + Broker-Dealer 牌照下运营 +/// @dev 使用 Transparent Proxy 部署 +contract CouponBackedSecurity is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, ERC721Holder { + bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE"); + bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE"); + bytes32 public constant RATING_AGENCY_ROLE = keccak256("RATING_AGENCY_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + + ICompliance public compliance; + ICoupon public couponContract; + IERC20 public stablecoin; + + struct Pool { + uint256[] couponIds; // 底层券资产 + uint256 totalFaceValue; // 底层面值总额 + uint256 totalShares; // 份额总数 + uint256 soldShares; // 已售份额 + uint256 maturityDate; // 到期日 + string creditRating; // 信用评级 + address issuer; // 池创建方 + bool active; // 是否活跃 + uint256 createdAt; + } + + mapping(uint256 => Pool) private _pools; + // 份额持有:poolId → holder → shares + mapping(uint256 => mapping(address => uint256)) public shares; + // 收益分配记录:poolId → holder → claimed amount + mapping(uint256 => mapping(address => uint256)) public claimedYield; + // 池的总收益 + mapping(uint256 => uint256) public poolYield; + + uint256 public nextPoolId; + + // --- Events --- + event PoolCreated( + uint256 indexed poolId, + address indexed issuer, + uint256 couponCount, + uint256 totalFaceValue, + uint256 totalShares + ); + event SharesPurchased(uint256 indexed poolId, address indexed buyer, uint256 shareCount, uint256 amount); + event CreditRatingSet(uint256 indexed poolId, string rating); + event YieldDeposited(uint256 indexed poolId, uint256 totalYield); + event YieldClaimed(uint256 indexed poolId, address indexed holder, uint256 amount); + event PoolDeactivated(uint256 indexed poolId); + + function initialize( + address _couponContract, + address _compliance, + address _stablecoin, + address admin + ) external initializer { + __AccessControl_init(); + __ReentrancyGuard_init(); + + couponContract = ICoupon(_couponContract); + compliance = ICompliance(_compliance); + stablecoin = IERC20(_stablecoin); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + } + + // ======================== + // 池管理 + // ======================== + + /// @notice 创建 CBS 池 — 将一组券的收益流打包 + /// @param couponIds 底层券 token IDs + /// @param totalShares 份额总数 + /// @param maturityDate 到期日 + function createPool( + uint256[] calldata couponIds, + uint256 totalShares, + uint256 maturityDate + ) external onlyRole(ISSUER_ROLE) nonReentrant returns (uint256 poolId) { + require(couponIds.length > 0, "CBS: empty pool"); + require(totalShares > 0, "CBS: zero shares"); + require(maturityDate > block.timestamp, "CBS: invalid maturity"); + + // 合规检查:创建者必须 KYC L3 + Broker-Dealer 资质 + compliance.requireKycLevel(msg.sender, 3); + + uint256 totalFace = 0; + for (uint256 i = 0; i < couponIds.length; i++) { + ICoupon.CouponConfig memory config = couponContract.getConfig(couponIds[i]); + require( + config.couponType == ICoupon.CouponType.Security, + "CBS: only Security coupons" + ); + totalFace += couponContract.getFaceValue(couponIds[i]); + // 锁定底层券到池中 + couponContract.safeTransferFrom(msg.sender, address(this), couponIds[i]); + } + + poolId = nextPoolId++; + _pools[poolId].couponIds = couponIds; + _pools[poolId].totalFaceValue = totalFace; + _pools[poolId].totalShares = totalShares; + _pools[poolId].soldShares = 0; + _pools[poolId].maturityDate = maturityDate; + _pools[poolId].creditRating = ""; + _pools[poolId].issuer = msg.sender; + _pools[poolId].active = true; + _pools[poolId].createdAt = block.timestamp; + + emit PoolCreated(poolId, msg.sender, couponIds.length, totalFace, totalShares); + } + + /// @notice 购买 CBS 份额 — 仅合格投资者(KYC L2+) + function purchaseShares(uint256 poolId, uint256 shareCount) external nonReentrant { + require(_pools[poolId].active, "CBS: pool not active"); + require(shareCount > 0, "CBS: zero shares"); + require( + _pools[poolId].soldShares + shareCount <= _pools[poolId].totalShares, + "CBS: insufficient shares" + ); + + // 合规检查 + compliance.requireKycLevel(msg.sender, 2); + + uint256 price = (_pools[poolId].totalFaceValue * shareCount) / _pools[poolId].totalShares; + require(price > 0, "CBS: price is zero"); + + stablecoin.transferFrom(msg.sender, address(this), price); + shares[poolId][msg.sender] += shareCount; + _pools[poolId].soldShares += shareCount; + + emit SharesPurchased(poolId, msg.sender, shareCount, price); + } + + // ======================== + // 信用评级 + // ======================== + + /// @notice 链下评级机构写入信用评级 + function setCreditRating(uint256 poolId, string calldata rating) + external + onlyRole(RATING_AGENCY_ROLE) + { + require(_pools[poolId].active, "CBS: pool not active"); + _pools[poolId].creditRating = rating; + emit CreditRatingSet(poolId, rating); + } + + // ======================== + // 收益分配 + // ======================== + + /// @notice 存入收益(由 clearing-service 链下计算后调用) + function depositYield(uint256 poolId, uint256 totalYield) external onlyRole(SETTLER_ROLE) nonReentrant { + require(_pools[poolId].active, "CBS: pool not active"); + require(totalYield > 0, "CBS: zero yield"); + + stablecoin.transferFrom(msg.sender, address(this), totalYield); + poolYield[poolId] += totalYield; + + emit YieldDeposited(poolId, totalYield); + } + + /// @notice 持有人领取收益 — 按份额比例 + function claimYield(uint256 poolId) external nonReentrant { + uint256 holderShares = shares[poolId][msg.sender]; + require(holderShares > 0, "CBS: no shares"); + + uint256 totalEntitled = (poolYield[poolId] * holderShares) / _pools[poolId].totalShares; + uint256 alreadyClaimed = claimedYield[poolId][msg.sender]; + uint256 claimable = totalEntitled - alreadyClaimed; + require(claimable > 0, "CBS: nothing to claim"); + + claimedYield[poolId][msg.sender] = totalEntitled; + stablecoin.transfer(msg.sender, claimable); + + emit YieldClaimed(poolId, msg.sender, claimable); + } + + /// @notice 停用池 + function deactivatePool(uint256 poolId) external onlyRole(GOVERNANCE_ROLE) { + _pools[poolId].active = false; + emit PoolDeactivated(poolId); + } + + // ======================== + // 查询 + // ======================== + + /// @notice 获取池信息 + function getPool(uint256 poolId) external view returns ( + uint256 totalFaceValue, + uint256 totalShares, + uint256 soldShares, + uint256 maturityDate, + string memory creditRating, + address issuer, + bool active + ) { + Pool storage p = _pools[poolId]; + return (p.totalFaceValue, p.totalShares, p.soldShares, p.maturityDate, p.creditRating, p.issuer, p.active); + } + + /// @notice 获取池中的券 IDs + function getPoolCouponIds(uint256 poolId) external view returns (uint256[] memory) { + return _pools[poolId].couponIds; + } + + /// @notice 计算持有人可领取的收益 + function getClaimableYield(uint256 poolId, address holder) external view returns (uint256) { + uint256 holderShares = shares[poolId][holder]; + if (holderShares == 0 || _pools[poolId].totalShares == 0) return 0; + + uint256 totalEntitled = (poolYield[poolId] * holderShares) / _pools[poolId].totalShares; + return totalEntitled - claimedYield[poolId][holder]; + } +} diff --git a/blockchain/genex-contracts/src/CouponFactory.sol b/blockchain/genex-contracts/src/CouponFactory.sol new file mode 100644 index 0000000..162707e --- /dev/null +++ b/blockchain/genex-contracts/src/CouponFactory.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "./interfaces/ICoupon.sol"; +import "./interfaces/ICompliance.sol"; + +/// @title CouponFactory — 券发行工厂 +/// @notice 负责批量铸造券 NFT,支持 Utility/Security 双轨制 +/// @dev 使用 Transparent Proxy 部署,可升级 +contract CouponFactory is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + ICoupon public couponContract; + ICompliance public compliance; + + uint256 public nextBatchId; + + // 批次信息 + struct BatchInfo { + address issuer; + uint256 faceValue; + uint256 quantity; + ICoupon.CouponType couponType; + uint256 createdAt; + uint256[] tokenIds; + } + + mapping(uint256 => BatchInfo) public batches; + + // --- Events --- + event CouponBatchMinted( + address indexed issuer, + uint256 indexed batchId, + uint256 faceValue, + uint256 quantity, + ICoupon.CouponType couponType + ); + event CouponContractUpdated(address indexed newCouponContract); + event ComplianceContractUpdated(address indexed newCompliance); + + function initialize( + address _couponContract, + address _compliance, + address admin + ) external initializer { + __AccessControl_init(); + __ReentrancyGuard_init(); + + couponContract = ICoupon(_couponContract); + compliance = ICompliance(_compliance); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(ADMIN_ROLE, admin); + _grantRole(MINTER_ROLE, admin); + } + + /// @notice 批量铸造券 + /// @param issuer 发行方地址 + /// @param faceValue 面值(USDC 精度,6位小数) + /// @param quantity 数量 + /// @param config 券配置 + /// @return tokenIds 铸造的 token ID 数组 + function mintBatch( + address issuer, + uint256 faceValue, + uint256 quantity, + ICoupon.CouponConfig calldata config + ) external onlyRole(MINTER_ROLE) nonReentrant returns (uint256[] memory tokenIds) { + require(issuer != address(0), "CouponFactory: zero address issuer"); + require(faceValue > 0, "CouponFactory: zero face value"); + require(quantity > 0 && quantity <= 10000, "CouponFactory: invalid quantity"); + + // Utility Track 强制:价格上限 = 面值,最长12个月 + if (config.couponType == ICoupon.CouponType.Utility) { + require(config.maxPrice <= faceValue, "Utility: maxPrice <= faceValue"); + require( + config.expiryDate <= block.timestamp + 365 days, + "Utility: max 12 months" + ); + require(config.expiryDate > block.timestamp, "Utility: expiry must be future"); + } + + // Securities Track 强制:必须通过合格投资者验证 + if (config.couponType == ICoupon.CouponType.Security) { + require(config.expiryDate > 0, "Security: expiry required"); + // 发行方必须 KYC L3 + compliance.requireKycLevel(issuer, 3); + } + + // 铸造 + tokenIds = new uint256[](quantity); + ICoupon.CouponConfig memory mintConfig = ICoupon.CouponConfig({ + couponType: config.couponType, + transferable: config.transferable, + maxResaleCount: config.maxResaleCount, + maxPrice: config.maxPrice, + expiryDate: config.expiryDate, + minPurchase: config.minPurchase, + stackable: config.stackable, + allowedStores: config.allowedStores, + issuer: issuer, + faceValue: faceValue + }); + + for (uint256 i = 0; i < quantity; i++) { + tokenIds[i] = couponContract.mint(issuer, faceValue, mintConfig); + } + + // 记录批次 + uint256 batchId = nextBatchId++; + batches[batchId] = BatchInfo({ + issuer: issuer, + faceValue: faceValue, + quantity: quantity, + couponType: config.couponType, + createdAt: block.timestamp, + tokenIds: tokenIds + }); + + emit CouponBatchMinted(issuer, batchId, faceValue, quantity, config.couponType); + } + + /// @notice 查询批次信息 + function getBatchInfo(uint256 batchId) external view returns (BatchInfo memory) { + return batches[batchId]; + } + + /// @notice 更新 Coupon 合约地址 + function setCouponContract(address _couponContract) external onlyRole(ADMIN_ROLE) { + require(_couponContract != address(0), "CouponFactory: zero address"); + couponContract = ICoupon(_couponContract); + emit CouponContractUpdated(_couponContract); + } + + /// @notice 更新 Compliance 合约地址 + function setComplianceContract(address _compliance) external onlyRole(ADMIN_ROLE) { + require(_compliance != address(0), "CouponFactory: zero address"); + compliance = ICompliance(_compliance); + emit ComplianceContractUpdated(_compliance); + } +} diff --git a/blockchain/genex-contracts/src/ExchangeRateOracle.sol b/blockchain/genex-contracts/src/ExchangeRateOracle.sol new file mode 100644 index 0000000..c7b3c31 --- /dev/null +++ b/blockchain/genex-contracts/src/ExchangeRateOracle.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "./interfaces/IChainlinkPriceFeed.sol"; + +/// @title ExchangeRateOracle — 汇率预言机集成 +/// @notice 集成 Chainlink Price Feed,提供各币种对 USD 汇率 +/// @dev 支持多币种,内置过期保护(最多15分钟) +contract ExchangeRateOracle is Initializable, AccessControlUpgradeable { + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + // 币种 → Chainlink Price Feed 地址 + mapping(string => address) public priceFeeds; + // 支持的币种列表 + string[] public supportedCurrencies; + + // 汇率数据最大容许延迟(秒) + uint256 public maxStaleness; + + // --- Events --- + event PriceFeedSet(string indexed currency, address feed); + event PriceFeedRemoved(string indexed currency); + event MaxStalenessUpdated(uint256 oldValue, uint256 newValue); + + function initialize(uint256 _maxStaleness, address admin) external initializer { + __AccessControl_init(); + + maxStaleness = _maxStaleness > 0 ? _maxStaleness : 900; // 默认 15 分钟 + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(ADMIN_ROLE, admin); + } + + /// @notice 获取某币种对 USD 的汇率 + /// @param currency 币种标识(如 "CNY", "JPY", "EUR") + /// @return rate 汇率(Chainlink 精度,通常 8 位小数) + function getRate(string calldata currency) external view returns (uint256 rate) { + address feed = priceFeeds[currency]; + require(feed != address(0), "Oracle: unsupported currency"); + + (, int256 answer,, uint256 updatedAt,) = IChainlinkPriceFeed(feed).latestRoundData(); + require(answer > 0, "Oracle: invalid price"); + require(block.timestamp - updatedAt <= maxStaleness, "Oracle: stale price data"); + + rate = uint256(answer); + } + + /// @notice 获取汇率及详细信息 + function getRateDetails(string calldata currency) + external + view + returns (uint256 rate, uint256 updatedAt, uint8 decimals) + { + address feed = priceFeeds[currency]; + require(feed != address(0), "Oracle: unsupported currency"); + + (, int256 answer,, uint256 _updatedAt,) = IChainlinkPriceFeed(feed).latestRoundData(); + require(answer > 0, "Oracle: invalid price"); + + rate = uint256(answer); + updatedAt = _updatedAt; + decimals = IChainlinkPriceFeed(feed).decimals(); + } + + /// @notice 设置币种的 Price Feed 地址 + function setPriceFeed(string calldata currency, address feed) external onlyRole(ADMIN_ROLE) { + require(feed != address(0), "Oracle: zero address"); + require(bytes(currency).length > 0, "Oracle: empty currency"); + + // 新增还是更新 + if (priceFeeds[currency] == address(0)) { + supportedCurrencies.push(currency); + } + + priceFeeds[currency] = feed; + emit PriceFeedSet(currency, feed); + } + + /// @notice 移除币种 Price Feed + function removePriceFeed(string calldata currency) external onlyRole(ADMIN_ROLE) { + require(priceFeeds[currency] != address(0), "Oracle: feed not found"); + priceFeeds[currency] = address(0); + emit PriceFeedRemoved(currency); + } + + /// @notice 更新最大过期时间 + function setMaxStaleness(uint256 _maxStaleness) external onlyRole(ADMIN_ROLE) { + require(_maxStaleness > 0, "Oracle: zero staleness"); + uint256 old = maxStaleness; + maxStaleness = _maxStaleness; + emit MaxStalenessUpdated(old, _maxStaleness); + } + + /// @notice 获取支持的币种数量 + function supportedCurrencyCount() external view returns (uint256) { + return supportedCurrencies.length; + } +} diff --git a/blockchain/genex-contracts/src/Governance.sol b/blockchain/genex-contracts/src/Governance.sol new file mode 100644 index 0000000..9d3b062 --- /dev/null +++ b/blockchain/genex-contracts/src/Governance.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; + +/// @title Governance — 治理合约 +/// @notice 3/5 多签 + 48h 时间锁 + 紧急 4h 通道 + 合约升级回滚 +/// @dev 管理所有合约的升级、参数调整、紧急冻结 +contract Governance is Initializable, AccessControlUpgradeable { + bytes32 public constant MULTISIG_ROLE = keccak256("MULTISIG_ROLE"); + + uint256 public constant REQUIRED_SIGNATURES = 3; // 3/5 多签 + uint256 public constant EMERGENCY_SIGNATURES = 4; // 紧急需 4/5 + uint256 public constant TIMELOCK = 48 hours; // 标准时间锁 + uint256 public constant EMERGENCY_TIMELOCK = 4 hours; // 紧急时间锁 + + uint256 public nextProposalId; + + struct Proposal { + bytes callData; + address target; + uint256 executeAfter; + uint256 approvalCount; + bool executed; + bool emergency; + address proposer; + string description; + uint256 createdAt; + } + + mapping(uint256 => Proposal) public proposals; + mapping(uint256 => mapping(address => bool)) public approvals; + + // 合约升级回滚支持 + mapping(address => address) public previousImplementations; + + // --- Events --- + event ProposalCreated( + uint256 indexed proposalId, + address indexed proposer, + address target, + bool emergency, + string description + ); + event ProposalApproved(uint256 indexed proposalId, address indexed approver, uint256 approvalCount); + event ProposalExecuted(uint256 indexed proposalId, address indexed executor); + event ProposalCancelled(uint256 indexed proposalId); + event ContractRolledBack(address indexed proxy, address indexed previousImpl); + event PreviousImplementationRecorded(address indexed proxy, address indexed impl); + + function initialize(address[] memory multisigMembers, address admin) external initializer { + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + for (uint256 i = 0; i < multisigMembers.length; i++) { + _grantRole(MULTISIG_ROLE, multisigMembers[i]); + } + } + + // ======================== + // 提案管理 + // ======================== + + /// @notice 创建标准提案(48h 时间锁) + function propose( + address target, + bytes calldata data, + string calldata description + ) external onlyRole(MULTISIG_ROLE) returns (uint256 proposalId) { + proposalId = _createProposal(target, data, description, false); + } + + /// @notice 创建紧急提案(4h 时间锁) + function proposeEmergency( + address target, + bytes calldata data, + string calldata description + ) external onlyRole(MULTISIG_ROLE) returns (uint256 proposalId) { + proposalId = _createProposal(target, data, description, true); + } + + /// @notice 审批提案 + function approve(uint256 proposalId) external onlyRole(MULTISIG_ROLE) { + Proposal storage p = proposals[proposalId]; + require(p.target != address(0), "Governance: proposal not found"); + require(!p.executed, "Governance: already executed"); + require(!approvals[proposalId][msg.sender], "Governance: already approved"); + + approvals[proposalId][msg.sender] = true; + p.approvalCount++; + + emit ProposalApproved(proposalId, msg.sender, p.approvalCount); + } + + /// @notice 执行提案 + function execute(uint256 proposalId) external onlyRole(MULTISIG_ROLE) { + Proposal storage p = proposals[proposalId]; + require(p.target != address(0), "Governance: proposal not found"); + require(!p.executed, "Governance: already executed"); + + // 签名数量检查 + uint256 required = p.emergency ? EMERGENCY_SIGNATURES : REQUIRED_SIGNATURES; + require(p.approvalCount >= required, "Governance: not enough approvals"); + + // 时间锁检查 + require(block.timestamp >= p.executeAfter, "Governance: timelock not expired"); + + p.executed = true; + (bool success,) = p.target.call(p.callData); + require(success, "Governance: execution failed"); + + emit ProposalExecuted(proposalId, msg.sender); + } + + /// @notice 取消提案(仅提案者或 Admin) + function cancel(uint256 proposalId) external { + Proposal storage p = proposals[proposalId]; + require(p.target != address(0), "Governance: proposal not found"); + require(!p.executed, "Governance: already executed"); + require( + p.proposer == msg.sender || hasRole(DEFAULT_ADMIN_ROLE, msg.sender), + "Governance: not authorized" + ); + + // 标记为已执行来阻止未来执行 + p.executed = true; + emit ProposalCancelled(proposalId); + } + + // ======================== + // 合约升级回滚 + // ======================== + + /// @notice 升级时记录前一版本 Implementation 地址 + function recordPreviousImplementation(address proxy, address currentImpl) + external + onlyRole(MULTISIG_ROLE) + { + previousImplementations[proxy] = currentImpl; + emit PreviousImplementationRecorded(proxy, currentImpl); + } + + /// @notice 紧急回滚至上一版本(需 4/5 多签) + /// @dev 通过紧急提案调用此方法 + function rollback(address proxy) external onlyRole(MULTISIG_ROLE) { + address prevImpl = previousImplementations[proxy]; + require(prevImpl != address(0), "Governance: no previous version"); + + // 调用 proxy 的 upgradeTo + (bool success,) = proxy.call( + abi.encodeWithSignature("upgradeTo(address)", prevImpl) + ); + require(success, "Governance: rollback failed"); + + emit ContractRolledBack(proxy, prevImpl); + } + + // ======================== + // 查询 + // ======================== + + /// @notice 查询提案详情 + function getProposal(uint256 proposalId) external view returns ( + address target, + uint256 executeAfter, + uint256 approvalCount, + bool executed, + bool emergency, + address proposer, + string memory description + ) { + Proposal storage p = proposals[proposalId]; + return (p.target, p.executeAfter, p.approvalCount, p.executed, p.emergency, p.proposer, p.description); + } + + /// @notice 查询某地址是否已审批 + function hasApproved(uint256 proposalId, address member) external view returns (bool) { + return approvals[proposalId][member]; + } + + // ======================== + // Internal + // ======================== + + function _createProposal( + address target, + bytes calldata data, + string calldata description, + bool emergency + ) internal returns (uint256 proposalId) { + require(target != address(0), "Governance: zero target"); + + proposalId = nextProposalId++; + uint256 timelock = emergency ? EMERGENCY_TIMELOCK : TIMELOCK; + + proposals[proposalId] = Proposal({ + callData: data, + target: target, + executeAfter: block.timestamp + timelock, + approvalCount: 1, // 提案者自动审批 + executed: false, + emergency: emergency, + proposer: msg.sender, + description: description, + createdAt: block.timestamp + }); + + approvals[proposalId][msg.sender] = true; + + emit ProposalCreated(proposalId, msg.sender, target, emergency, description); + } +} diff --git a/blockchain/genex-contracts/src/Redemption.sol b/blockchain/genex-contracts/src/Redemption.sol new file mode 100644 index 0000000..c1a1220 --- /dev/null +++ b/blockchain/genex-contracts/src/Redemption.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "./interfaces/ICoupon.sol"; +import "./interfaces/ICompliance.sol"; + +/// @title Redemption — 兑付合约 +/// @notice 消费者直接与发行方结算,平台不介入;兑付时销毁券 +/// @dev 使用 Transparent Proxy 部署 +contract Redemption is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + ICoupon public couponContract; + ICompliance public compliance; + + // 兑付记录 + struct RedemptionRecord { + uint256 tokenId; + address consumer; + address issuer; + bytes32 storeId; + uint256 redeemedAt; + } + + uint256 public totalRedemptions; + mapping(uint256 => RedemptionRecord) public redemptions; // redemptionId → record + + // --- Events --- + event CouponRedeemed( + uint256 indexed tokenId, + address indexed consumer, + address indexed issuer, + bytes32 storeId, + uint256 redemptionId + ); + + function initialize( + address _couponContract, + address _compliance, + address admin + ) external initializer { + __AccessControl_init(); + __ReentrancyGuard_init(); + + couponContract = ICoupon(_couponContract); + compliance = ICompliance(_compliance); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(ADMIN_ROLE, admin); + } + + /// @notice 消费者兑付券 — 销毁 NFT,触发事件通知发行方 + /// @param tokenId 券 token ID + /// @param storeId 门店 ID(bytes32 哈希) + function redeem(uint256 tokenId, bytes32 storeId) external nonReentrant { + // 所有权验证 + require(couponContract.ownerOf(tokenId) == msg.sender, "Redemption: not owner"); + + // 合规检查 + require(!compliance.isBlacklisted(msg.sender), "Redemption: blacklisted"); + + ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId); + + // 到期验证 + require(block.timestamp <= config.expiryDate, "Redemption: coupon expired"); + + // 门店限定验证 + if (config.allowedStores.length > 0) { + require(_isAllowedStore(storeId, config.allowedStores), "Redemption: store not allowed"); + } + + // 销毁券(burn) + couponContract.burn(tokenId); + + // 记录兑付 + uint256 redemptionId = totalRedemptions++; + redemptions[redemptionId] = RedemptionRecord({ + tokenId: tokenId, + consumer: msg.sender, + issuer: config.issuer, + storeId: storeId, + redeemedAt: block.timestamp + }); + + // 通知发行方(event),平台不介入消费数据 + emit CouponRedeemed(tokenId, msg.sender, config.issuer, storeId, redemptionId); + } + + /// @notice 查询兑付记录 + function getRedemption(uint256 redemptionId) external view returns (RedemptionRecord memory) { + return redemptions[redemptionId]; + } + + // --- Internal --- + + /// @dev 检查门店是否在允许列表中 + function _isAllowedStore(bytes32 storeId, bytes32[] memory allowedStores) internal pure returns (bool) { + for (uint256 i = 0; i < allowedStores.length; i++) { + if (allowedStores[i] == storeId) return true; + } + return false; + } +} diff --git a/blockchain/genex-contracts/src/Settlement.sol b/blockchain/genex-contracts/src/Settlement.sol new file mode 100644 index 0000000..9318b72 --- /dev/null +++ b/blockchain/genex-contracts/src/Settlement.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./interfaces/ICoupon.sol"; +import "./interfaces/ICompliance.sol"; + +/// @title Settlement — 交易结算合约 +/// @notice 原子交换:券 ↔ 稳定币 同时转移,支持多稳定币 +/// @dev 使用 Transparent Proxy 部署 +contract Settlement is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { + bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + + ICoupon public couponContract; + ICompliance public compliance; + + // 多稳定币支持 + mapping(address => bool) public supportedStablecoins; + address public defaultStablecoin; // USDC + + // --- Events --- + event TradeSettled( + uint256 indexed tokenId, + address indexed buyer, + address indexed seller, + uint256 price, + address stablecoin + ); + event RefundExecuted( + uint256 indexed tokenId, + address indexed buyer, + address indexed seller, + uint256 refundAmount, + address stablecoin + ); + event StablecoinAdded(address indexed token); + event StablecoinRemoved(address indexed token); + + function initialize( + address _couponContract, + address _compliance, + address _defaultStablecoin, + address admin + ) external initializer { + __AccessControl_init(); + __ReentrancyGuard_init(); + + couponContract = ICoupon(_couponContract); + compliance = ICompliance(_compliance); + defaultStablecoin = _defaultStablecoin; + supportedStablecoins[_defaultStablecoin] = true; + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(SETTLER_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + } + + /// @notice 原子交换:券 ↔ 稳定币 同时转移 + /// @param tokenId 券 token ID + /// @param buyer 买方地址 + /// @param seller 卖方地址 + /// @param price 成交价格(USDC 精度) + /// @param stablecoin 使用的稳定币地址 + function executeSwap( + uint256 tokenId, + address buyer, + address seller, + uint256 price, + address stablecoin + ) external onlyRole(SETTLER_ROLE) nonReentrant { + require(supportedStablecoins[stablecoin], "Settlement: unsupported stablecoin"); + require(buyer != address(0) && seller != address(0), "Settlement: zero address"); + require(price > 0, "Settlement: zero price"); + + // 合规检查 + ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId); + compliance.preTradeCheck(buyer, seller, price, config.couponType); + + // Utility Track: 价格 ≤ 面值上限 + if (config.couponType == ICoupon.CouponType.Utility) { + require(price <= config.maxPrice, "Utility: price exceeds max price"); + } + + // 转售次数检查 + require( + couponContract.getResaleCount(tokenId) < config.maxResaleCount, + "Settlement: max resale count exceeded" + ); + + // 到期检查 + require(block.timestamp <= config.expiryDate, "Settlement: coupon expired"); + + // 原子交换(要么全部成功,要么全部回滚) + IERC20(stablecoin).transferFrom(buyer, seller, price); + couponContract.safeTransferFrom(seller, buyer, tokenId); + couponContract.incrementResaleCount(tokenId); + + emit TradeSettled(tokenId, buyer, seller, price, stablecoin); + } + + /// @notice 使用默认稳定币的原子交换(便捷方法) + function executeSwap( + uint256 tokenId, + address buyer, + address seller, + uint256 price + ) external onlyRole(SETTLER_ROLE) nonReentrant { + _executeSwapInternal(tokenId, buyer, seller, price, defaultStablecoin); + } + + /// @notice 退款:反向原子交换 + function executeRefund( + uint256 tokenId, + address buyer, + address seller, + uint256 refundAmount, + address stablecoin + ) external onlyRole(SETTLER_ROLE) nonReentrant { + require(supportedStablecoins[stablecoin], "Settlement: unsupported stablecoin"); + require(refundAmount > 0, "Settlement: zero refund"); + + // 合规检查 + require(!compliance.isBlacklisted(buyer), "Settlement: buyer blacklisted"); + require(!compliance.isBlacklisted(seller), "Settlement: seller blacklisted"); + + // 反向原子交换 + couponContract.safeTransferFrom(buyer, seller, tokenId); + IERC20(stablecoin).transferFrom(seller, buyer, refundAmount); + + emit RefundExecuted(tokenId, buyer, seller, refundAmount, stablecoin); + } + + /// @notice 添加支持的稳定币 + function addStablecoin(address token) external onlyRole(GOVERNANCE_ROLE) { + require(token != address(0), "Settlement: zero address"); + supportedStablecoins[token] = true; + emit StablecoinAdded(token); + } + + /// @notice 移除稳定币支持 + function removeStablecoin(address token) external onlyRole(GOVERNANCE_ROLE) { + require(token != defaultStablecoin, "Settlement: cannot remove default"); + supportedStablecoins[token] = false; + emit StablecoinRemoved(token); + } + + // --- Internal --- + + function _executeSwapInternal( + uint256 tokenId, + address buyer, + address seller, + uint256 price, + address stablecoin + ) internal { + require(buyer != address(0) && seller != address(0), "Settlement: zero address"); + require(price > 0, "Settlement: zero price"); + + ICoupon.CouponConfig memory config = couponContract.getConfig(tokenId); + compliance.preTradeCheck(buyer, seller, price, config.couponType); + + if (config.couponType == ICoupon.CouponType.Utility) { + require(price <= config.maxPrice, "Utility: price exceeds max price"); + } + + require( + couponContract.getResaleCount(tokenId) < config.maxResaleCount, + "Settlement: max resale count exceeded" + ); + require(block.timestamp <= config.expiryDate, "Settlement: coupon expired"); + + IERC20(stablecoin).transferFrom(buyer, seller, price); + couponContract.safeTransferFrom(seller, buyer, tokenId); + couponContract.incrementResaleCount(tokenId); + + emit TradeSettled(tokenId, buyer, seller, price, stablecoin); + } +} diff --git a/blockchain/genex-contracts/src/Treasury.sol b/blockchain/genex-contracts/src/Treasury.sol new file mode 100644 index 0000000..1382496 --- /dev/null +++ b/blockchain/genex-contracts/src/Treasury.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Treasury — 资金托管合约 +/// @notice 保障资金锁定、交易 Escrow、资金释放与费用分割 +/// @dev 使用 Transparent Proxy 部署 +contract Treasury is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { + bytes32 public constant SETTLER_ROLE = keccak256("SETTLER_ROLE"); + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + + IERC20 public stablecoin; + address public platformFeeCollector; + + // 发行方保障资金(自愿缴纳,提升信用评级) + mapping(address => uint256) public guaranteeFunds; + // 发行方冻结的销售款(自愿,作为兑付保障) + mapping(address => uint256) public frozenSalesRevenue; + // Escrow 托管 + mapping(uint256 => EscrowInfo) public escrows; + + struct EscrowInfo { + address buyer; + uint256 amount; + bool released; + bool refunded; + } + + // --- Events --- + event GuaranteeFundDeposited(address indexed issuer, uint256 amount); + event GuaranteeFundWithdrawn(address indexed issuer, uint256 amount); + event GuaranteeFundActivated(address indexed issuer, uint256 totalClaim); + event SalesRevenueFrozen(address indexed issuer, uint256 amount); + event SalesRevenueReleased(address indexed issuer, uint256 amount); + event FundsEscrowed(uint256 indexed orderId, address indexed buyer, uint256 amount); + event FundsReleased(uint256 indexed orderId, address indexed seller, uint256 amount, uint256 platformFee); + event FundsRefunded(uint256 indexed orderId, address indexed buyer, uint256 amount); + + function initialize( + address _stablecoin, + address _platformFeeCollector, + address admin + ) external initializer { + __AccessControl_init(); + __ReentrancyGuard_init(); + + stablecoin = IERC20(_stablecoin); + platformFeeCollector = _platformFeeCollector; + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(SETTLER_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + } + + // ======================== + // 保障资金管理 + // ======================== + + /// @notice 发行方缴纳保障资金 + function depositGuaranteeFund(uint256 amount) external nonReentrant { + require(amount > 0, "Treasury: zero amount"); + stablecoin.transferFrom(msg.sender, address(this), amount); + guaranteeFunds[msg.sender] += amount; + emit GuaranteeFundDeposited(msg.sender, amount); + } + + /// @notice 发行方提取保障资金(需治理审批) + function withdrawGuaranteeFund(address issuer, uint256 amount) external onlyRole(GOVERNANCE_ROLE) nonReentrant { + require(guaranteeFunds[issuer] >= amount, "Treasury: insufficient guarantee"); + guaranteeFunds[issuer] -= amount; + stablecoin.transfer(issuer, amount); + emit GuaranteeFundWithdrawn(issuer, amount); + } + + /// @notice 发行方违约时启用保障资金赔付 + function activateGuaranteeFund( + address issuer, + address[] calldata claimants, + uint256[] calldata amounts + ) external onlyRole(GOVERNANCE_ROLE) nonReentrant { + require(claimants.length == amounts.length, "Treasury: length mismatch"); + + uint256 totalClaim = 0; + for (uint256 i = 0; i < claimants.length; i++) { + totalClaim += amounts[i]; + } + require(guaranteeFunds[issuer] >= totalClaim, "Treasury: insufficient guarantee fund"); + + guaranteeFunds[issuer] -= totalClaim; + for (uint256 i = 0; i < claimants.length; i++) { + stablecoin.transfer(claimants[i], amounts[i]); + } + + emit GuaranteeFundActivated(issuer, totalClaim); + } + + // ======================== + // 销售款冻结 + // ======================== + + /// @notice 发行方冻结销售款(自愿,作为兑付保障) + function freezeSalesRevenue(uint256 amount) external nonReentrant { + require(amount > 0, "Treasury: zero amount"); + stablecoin.transferFrom(msg.sender, address(this), amount); + frozenSalesRevenue[msg.sender] += amount; + emit SalesRevenueFrozen(msg.sender, amount); + } + + /// @notice 释放冻结销售款(需治理审批) + function releaseSalesRevenue(address issuer, uint256 amount) external onlyRole(GOVERNANCE_ROLE) nonReentrant { + require(frozenSalesRevenue[issuer] >= amount, "Treasury: insufficient frozen revenue"); + frozenSalesRevenue[issuer] -= amount; + stablecoin.transfer(issuer, amount); + emit SalesRevenueReleased(issuer, amount); + } + + // ======================== + // 交易 Escrow + // ======================== + + /// @notice 交易资金托管(原子交换中间态) + function escrow(uint256 orderId, address buyer, uint256 amount) external onlyRole(SETTLER_ROLE) nonReentrant { + require(amount > 0, "Treasury: zero escrow"); + require(escrows[orderId].amount == 0, "Treasury: escrow exists"); + + stablecoin.transferFrom(buyer, address(this), amount); + escrows[orderId] = EscrowInfo({ + buyer: buyer, + amount: amount, + released: false, + refunded: false + }); + + emit FundsEscrowed(orderId, buyer, amount); + } + + /// @notice 释放托管资金给卖方 + function release( + uint256 orderId, + address seller, + uint256 amount, + uint256 platformFee + ) external onlyRole(SETTLER_ROLE) nonReentrant { + EscrowInfo storage info = escrows[orderId]; + require(info.amount > 0, "Treasury: no escrow"); + require(!info.released && !info.refunded, "Treasury: already settled"); + require(amount + platformFee <= info.amount, "Treasury: exceeds escrow"); + + info.released = true; + + // 卖方收款 + stablecoin.transfer(seller, amount); + // 平台手续费归集 + if (platformFee > 0) { + stablecoin.transfer(platformFeeCollector, platformFee); + } + + emit FundsReleased(orderId, seller, amount, platformFee); + } + + /// @notice 退回托管资金给买方 + function refundEscrow(uint256 orderId) external onlyRole(SETTLER_ROLE) nonReentrant { + EscrowInfo storage info = escrows[orderId]; + require(info.amount > 0, "Treasury: no escrow"); + require(!info.released && !info.refunded, "Treasury: already settled"); + + info.refunded = true; + stablecoin.transfer(info.buyer, info.amount); + + emit FundsRefunded(orderId, info.buyer, info.amount); + } + + // ======================== + // 管理 + // ======================== + + /// @notice 更新平台手续费收集地址 + function setPlatformFeeCollector(address _collector) external onlyRole(GOVERNANCE_ROLE) { + require(_collector != address(0), "Treasury: zero address"); + platformFeeCollector = _collector; + } +} diff --git a/blockchain/genex-contracts/src/interfaces/IChainlinkPriceFeed.sol b/blockchain/genex-contracts/src/interfaces/IChainlinkPriceFeed.sol new file mode 100644 index 0000000..37da0a9 --- /dev/null +++ b/blockchain/genex-contracts/src/interfaces/IChainlinkPriceFeed.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title IChainlinkPriceFeed — Chainlink 预言机接口 +/// @notice 定义 Chainlink Price Feed 的标准调用接口 +interface IChainlinkPriceFeed { + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function decimals() external view returns (uint8); + function description() external view returns (string memory); +} diff --git a/blockchain/genex-contracts/src/interfaces/ICompliance.sol b/blockchain/genex-contracts/src/interfaces/ICompliance.sol new file mode 100644 index 0000000..b36d9c5 --- /dev/null +++ b/blockchain/genex-contracts/src/interfaces/ICompliance.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./ICoupon.sol"; + +/// @title ICompliance — 合规合约接口 +/// @notice 定义 OFAC、KYC、Travel Rule 检查的外部调用接口 +interface ICompliance { + function isBlacklisted(address account) external view returns (bool); + function isFrozen(address account) external view returns (bool); + function getKycLevel(address account) external view returns (uint8); + function requireKycLevel(address account, uint8 requiredLevel) external view; + + function preTradeCheck( + address buyer, + address seller, + uint256 amount, + ICoupon.CouponType couponType + ) external view; + + function p2pComplianceCheck( + address sender, + address receiver, + uint256 amount + ) external view; + + function recordTravelRule( + address sender, + address receiver, + bytes32 senderInfoHash, + bytes32 receiverInfoHash + ) external; +} diff --git a/blockchain/genex-contracts/src/interfaces/ICoupon.sol b/blockchain/genex-contracts/src/interfaces/ICoupon.sol new file mode 100644 index 0000000..fa0ac63 --- /dev/null +++ b/blockchain/genex-contracts/src/interfaces/ICoupon.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title ICoupon — 券 NFT 接口 +/// @notice 定义 Coupon ERC-721 合约的外部调用接口 +interface ICoupon { + enum CouponType { Utility, Security } + + struct CouponConfig { + CouponType couponType; + bool transferable; + uint8 maxResaleCount; + uint256 maxPrice; + uint256 expiryDate; + uint256 minPurchase; + bool stackable; + bytes32[] allowedStores; + address issuer; + uint256 faceValue; + } + + function getConfig(uint256 tokenId) external view returns (CouponConfig memory); + function getFaceValue(uint256 tokenId) external view returns (uint256); + function getResaleCount(uint256 tokenId) external view returns (uint256); + function incrementResaleCount(uint256 tokenId) external; + function ownerOf(uint256 tokenId) external view returns (address); + function burn(uint256 tokenId) external; + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function mint(address to, uint256 faceValue, CouponConfig calldata config) external returns (uint256 tokenId); +} diff --git a/blockchain/genex-contracts/test/Compliance.t.sol b/blockchain/genex-contracts/test/Compliance.t.sol new file mode 100644 index 0000000..9ef7ca5 --- /dev/null +++ b/blockchain/genex-contracts/test/Compliance.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Compliance.sol"; +import "../src/interfaces/ICoupon.sol"; + +contract ComplianceTest is Test { + Compliance compliance; + + address admin = address(1); + address user1 = address(2); + address user2 = address(3); + address officer = address(4); + + function setUp() public { + vm.startPrank(admin); + compliance = new Compliance(); + compliance.initialize(admin); + compliance.grantRole(keccak256("COMPLIANCE_OFFICER_ROLE"), officer); + vm.stopPrank(); + } + + // === OFAC === + + function test_AddToBlacklist() public { + vm.prank(officer); + compliance.addToBlacklist(user1, "OFAC SDN"); + assertTrue(compliance.isBlacklisted(user1)); + } + + function test_RemoveFromBlacklist() public { + vm.prank(officer); + compliance.addToBlacklist(user1, "OFAC SDN"); + + vm.prank(admin); + compliance.removeFromBlacklist(user1); + assertFalse(compliance.isBlacklisted(user1)); + } + + function test_RemoveFromBlacklistOnlyGovernance() public { + vm.prank(officer); + compliance.addToBlacklist(user1, "OFAC SDN"); + + vm.prank(officer); + vm.expectRevert(); + compliance.removeFromBlacklist(user1); + } + + // === KYC === + + function test_SetKycLevel() public { + vm.prank(admin); + compliance.setKycLevel(user1, 2); + assertEq(compliance.getKycLevel(user1), 2); + } + + function test_InvalidKycLevelReverts() public { + vm.prank(admin); + vm.expectRevert("Compliance: invalid KYC level"); + compliance.setKycLevel(user1, 4); + } + + function test_RequireKycLevelSuccess() public view { + // L0 默认 + compliance.requireKycLevel(user1, 0); + } + + function test_RequireKycLevelFails() public { + vm.expectRevert("Compliance: insufficient KYC level"); + compliance.requireKycLevel(user1, 1); + } + + function test_BatchSetKycLevel() public { + address[] memory accounts = new address[](2); + accounts[0] = user1; + accounts[1] = user2; + uint8[] memory levels = new uint8[](2); + levels[0] = 1; + levels[1] = 2; + + vm.prank(admin); + compliance.batchSetKycLevel(accounts, levels); + + assertEq(compliance.getKycLevel(user1), 1); + assertEq(compliance.getKycLevel(user2), 2); + } + + // === Freeze === + + function test_FreezeAccount() public { + vm.prank(admin); + compliance.freezeAccount(user1); + assertTrue(compliance.isFrozen(user1)); + } + + function test_UnfreezeAccount() public { + vm.prank(admin); + compliance.freezeAccount(user1); + vm.prank(admin); + compliance.unfreezeAccount(user1); + assertFalse(compliance.isFrozen(user1)); + } + + // === Travel Rule === + + function test_RecordTravelRule() public { + vm.prank(officer); + compliance.recordTravelRule(user1, user2, keccak256("sender"), keccak256("receiver")); + assertTrue(compliance.hasTravelRuleRecord(user1, user2)); + } + + // === Pre-Trade Check === + + function test_PreTradeCheckUtilityRequiresKycL1() public { + // 无 KYC + vm.expectRevert("Compliance: buyer needs KYC L1 for Utility"); + compliance.preTradeCheck(user1, user2, 100e6, ICoupon.CouponType.Utility); + } + + function test_PreTradeCheckSecurityRequiresKycL2() public { + vm.startPrank(admin); + compliance.setKycLevel(user1, 1); + compliance.setKycLevel(user2, 1); + vm.stopPrank(); + + vm.expectRevert("Compliance: buyer needs KYC L2 for Security"); + compliance.preTradeCheck(user1, user2, 100e6, ICoupon.CouponType.Security); + } + + function test_PreTradeCheckLargeTradeRequiresKycL2() public { + vm.startPrank(admin); + compliance.setKycLevel(user1, 1); + compliance.setKycLevel(user2, 1); + vm.stopPrank(); + + vm.expectRevert("Compliance: buyer needs KYC L2 for large trade"); + compliance.preTradeCheck(user1, user2, 10000e6, ICoupon.CouponType.Utility); + } + + function test_PreTradeCheckBlacklistedReverts() public { + vm.startPrank(admin); + compliance.setKycLevel(user1, 2); + compliance.setKycLevel(user2, 2); + vm.stopPrank(); + + vm.prank(officer); + compliance.addToBlacklist(user1, "OFAC"); + + vm.expectRevert("Compliance: buyer blacklisted"); + compliance.preTradeCheck(user1, user2, 100e6, ICoupon.CouponType.Utility); + } + + function test_PreTradeCheckFrozenReverts() public { + vm.startPrank(admin); + compliance.setKycLevel(user1, 2); + compliance.setKycLevel(user2, 2); + compliance.freezeAccount(user2); + vm.stopPrank(); + + vm.expectRevert("Compliance: seller frozen"); + compliance.preTradeCheck(user1, user2, 100e6, ICoupon.CouponType.Utility); + } + + // === P2P Check === + + function test_P2pCheckRequiresTravelRule() public { + vm.startPrank(admin); + compliance.setKycLevel(user1, 2); + compliance.setKycLevel(user2, 2); + vm.stopPrank(); + + vm.expectRevert("Compliance: Travel Rule data required"); + compliance.p2pComplianceCheck(user1, user2, 5000e6); + } + + function test_P2pCheckWithTravelRuleSuccess() public { + vm.startPrank(admin); + compliance.setKycLevel(user1, 2); + compliance.setKycLevel(user2, 2); + vm.stopPrank(); + + vm.prank(officer); + compliance.recordTravelRule(user1, user2, keccak256("s"), keccak256("r")); + + // 不应 revert + compliance.p2pComplianceCheck(user1, user2, 5000e6); + } + + function test_P2pCheckBelowThresholdNoTravelRule() public { + // $2,999 无需 Travel Rule + compliance.p2pComplianceCheck(user1, user2, 2999e6); + } +} diff --git a/blockchain/genex-contracts/test/Coupon.t.sol b/blockchain/genex-contracts/test/Coupon.t.sol new file mode 100644 index 0000000..991b8a4 --- /dev/null +++ b/blockchain/genex-contracts/test/Coupon.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Coupon.sol"; +import "../src/interfaces/ICoupon.sol"; + +contract CouponTest is Test { + Coupon coupon; + + address admin = address(1); + address factory = address(2); + address settler = address(3); + address user1 = address(4); + address user2 = address(5); + + function setUp() public { + vm.startPrank(admin); + coupon = new Coupon(); + coupon.initialize("Genex Coupon", "GXC", admin); + coupon.grantRole(keccak256("FACTORY_ROLE"), factory); + coupon.grantRole(keccak256("SETTLER_ROLE"), settler); + vm.stopPrank(); + } + + function test_MintSetsConfigCorrectly() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + ICoupon.CouponConfig memory config = coupon.getConfig(tokenId); + + assertEq(uint8(config.couponType), uint8(ICoupon.CouponType.Utility)); + assertTrue(config.transferable); + assertEq(config.maxResaleCount, 3); + assertEq(coupon.ownerOf(tokenId), user1); + assertEq(coupon.getFaceValue(tokenId), 100e6); + } + + function test_BurnByOwner() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + vm.prank(user1); + coupon.burn(tokenId); + vm.expectRevert(); + coupon.ownerOf(tokenId); + } + + function test_BurnBySettler() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + vm.prank(settler); + coupon.burn(tokenId); + } + + function test_NonTransferableCouponReverts() public { + uint256 tokenId = _mintNonTransferableCoupon(user1, 50e6); + + vm.prank(user1); + vm.expectRevert("Coupon: non-transferable"); + coupon.safeTransferFrom(user1, user2, tokenId); + } + + function test_TransferableTransferSucceeds() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + + vm.prank(user1); + coupon.safeTransferFrom(user1, user2, tokenId); + assertEq(coupon.ownerOf(tokenId), user2); + } + + function test_MaxResaleCountEnforced() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + + // 使用 settler 增加到上限 (3) + vm.startPrank(settler); + coupon.incrementResaleCount(tokenId); + coupon.incrementResaleCount(tokenId); + coupon.incrementResaleCount(tokenId); + vm.stopPrank(); + + assertEq(coupon.getResaleCount(tokenId), 3); + + // 转移应被拒绝 + vm.prank(user1); + vm.expectRevert("Coupon: max resale count exceeded"); + coupon.safeTransferFrom(user1, user2, tokenId); + } + + function test_IncrementResaleCountOnlySettler() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + + vm.prank(user1); + vm.expectRevert(); + coupon.incrementResaleCount(tokenId); + } + + function test_BatchTransfer() public { + uint256[] memory tokenIds = new uint256[](3); + for (uint256 i = 0; i < 3; i++) { + tokenIds[i] = _mintUtilityCoupon(user1, 100e6); + } + + vm.prank(user1); + coupon.batchTransfer(user1, user2, tokenIds); + + for (uint256 i = 0; i < 3; i++) { + assertEq(coupon.ownerOf(tokenIds[i]), user2); + } + } + + function test_ResaleCountInitiallyZero() public { + uint256 tokenId = _mintUtilityCoupon(user1, 100e6); + assertEq(coupon.getResaleCount(tokenId), 0); + } + + function test_MintIncrementsTokenId() public { + uint256 id1 = _mintUtilityCoupon(user1, 100e6); + uint256 id2 = _mintUtilityCoupon(user1, 200e6); + assertEq(id2, id1 + 1); + } + + // --- Helpers --- + + function _mintUtilityCoupon(address to, uint256 faceValue) internal returns (uint256) { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: faceValue, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: admin, + faceValue: faceValue + }); + + vm.prank(factory); + return coupon.mint(to, faceValue, config); + } + + function _mintNonTransferableCoupon(address to, uint256 faceValue) internal returns (uint256) { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: false, + maxResaleCount: 0, + maxPrice: faceValue, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: admin, + faceValue: faceValue + }); + + vm.prank(factory); + return coupon.mint(to, faceValue, config); + } +} diff --git a/blockchain/genex-contracts/test/CouponBackedSecurity.t.sol b/blockchain/genex-contracts/test/CouponBackedSecurity.t.sol new file mode 100644 index 0000000..1d4bc62 --- /dev/null +++ b/blockchain/genex-contracts/test/CouponBackedSecurity.t.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/CouponBackedSecurity.sol"; +import "../src/Coupon.sol"; +import "../src/Compliance.sol"; +import "../src/interfaces/ICoupon.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDC2 is ERC20 { + constructor() ERC20("USD Coin", "USDC") {} + function decimals() public pure override returns (uint8) { return 6; } + function mint(address to, uint256 amount) external { _mint(to, amount); } +} + +contract CouponBackedSecurityTest is Test { + CouponBackedSecurity cbs; + Coupon coupon; + Compliance compliance; + MockUSDC2 usdc; + + address admin = address(1); + address cbsIssuer = address(2); + address investor1 = address(3); + address investor2 = address(4); + address settler = address(5); + address ratingAgency = address(6); + + function setUp() public { + vm.startPrank(admin); + + usdc = new MockUSDC2(); + compliance = new Compliance(); + compliance.initialize(admin); + + coupon = new Coupon(); + coupon.initialize("Genex Coupon", "GXC", admin); + coupon.grantRole(keccak256("FACTORY_ROLE"), admin); + + cbs = new CouponBackedSecurity(); + cbs.initialize(address(coupon), address(compliance), address(usdc), admin); + cbs.grantRole(keccak256("ISSUER_ROLE"), cbsIssuer); + cbs.grantRole(keccak256("SETTLER_ROLE"), settler); + cbs.grantRole(keccak256("RATING_AGENCY_ROLE"), ratingAgency); + + // KYC + compliance.setKycLevel(cbsIssuer, 3); + compliance.setKycLevel(investor1, 2); + compliance.setKycLevel(investor2, 2); + + // Fund + usdc.mint(investor1, 1_000_000e6); + usdc.mint(investor2, 1_000_000e6); + usdc.mint(settler, 1_000_000e6); + + vm.stopPrank(); + + // Approvals + vm.prank(investor1); + usdc.approve(address(cbs), type(uint256).max); + vm.prank(investor2); + usdc.approve(address(cbs), type(uint256).max); + vm.prank(settler); + usdc.approve(address(cbs), type(uint256).max); + vm.prank(cbsIssuer); + coupon.setApprovalForAll(address(cbs), true); + } + + function test_CreatePool() public { + uint256[] memory tokenIds = _mintSecurityCoupons(3, 1000e6); + + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); + + ( + uint256 totalFaceValue, + uint256 totalShares, + uint256 soldShares, + , + , + address issuer, + bool active + ) = cbs.getPool(poolId); + + assertEq(totalFaceValue, 3000e6); + assertEq(totalShares, 100); + assertEq(soldShares, 0); + assertEq(issuer, cbsIssuer); + assertTrue(active); + } + + function test_PurchaseShares() public { + uint256[] memory tokenIds = _mintSecurityCoupons(2, 500e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); + + vm.prank(investor1); + cbs.purchaseShares(poolId, 30); + + assertEq(cbs.shares(poolId, investor1), 30); + (, , uint256 soldShares, , , , ) = cbs.getPool(poolId); + assertEq(soldShares, 30); + } + + function test_PurchaseSharesInsufficientReverts() public { + uint256[] memory tokenIds = _mintSecurityCoupons(1, 100e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); + + vm.prank(investor1); + vm.expectRevert("CBS: insufficient shares"); + cbs.purchaseShares(poolId, 11); + } + + function test_PurchaseSharesRequiresKycL2() public { + uint256[] memory tokenIds = _mintSecurityCoupons(1, 100e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); + + address lowKyc = address(99); + vm.prank(admin); + compliance.setKycLevel(lowKyc, 1); + + vm.prank(lowKyc); + vm.expectRevert("Compliance: insufficient KYC level"); + cbs.purchaseShares(poolId, 1); + } + + function test_OnlySecurityCouponsAllowed() public { + // Mint utility coupon + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: 100e6, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: cbsIssuer, + faceValue: 100e6 + }); + vm.prank(admin); + uint256 tokenId = coupon.mint(cbsIssuer, 100e6, config); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + vm.prank(cbsIssuer); + vm.expectRevert("CBS: only Security coupons"); + cbs.createPool(tokenIds, 10, block.timestamp + 365 days); + } + + function test_SetCreditRating() public { + uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); + + vm.prank(ratingAgency); + cbs.setCreditRating(poolId, "AAA"); + + (, , , , string memory rating, , ) = cbs.getPool(poolId); + assertEq(rating, "AAA"); + } + + function test_DepositAndClaimYield() public { + uint256[] memory tokenIds = _mintSecurityCoupons(2, 500e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); + + // 两个投资者各买 50 份 + vm.prank(investor1); + cbs.purchaseShares(poolId, 50); + vm.prank(investor2); + cbs.purchaseShares(poolId, 50); + + // Settler 存入收益 1000 USDC + vm.prank(settler); + cbs.depositYield(poolId, 1000e6); + + // 各自领取 500 USDC + vm.prank(investor1); + cbs.claimYield(poolId); + assertEq(usdc.balanceOf(investor1), 1_000_000e6 - 500e6 + 500e6); // 购买花了500e6, 领回500e6 + + vm.prank(investor2); + cbs.claimYield(poolId); + } + + function test_ClaimYieldNothingToClaimReverts() public { + uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); + + vm.prank(investor1); + cbs.purchaseShares(poolId, 5); + + // 没有收益存入 + vm.prank(investor1); + vm.expectRevert("CBS: nothing to claim"); + cbs.claimYield(poolId); + } + + function test_DeactivatePool() public { + uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 10, block.timestamp + 365 days); + + vm.prank(admin); + cbs.deactivatePool(poolId); + + (, , , , , , bool active) = cbs.getPool(poolId); + assertFalse(active); + } + + function test_GetClaimableYield() public { + uint256[] memory tokenIds = _mintSecurityCoupons(1, 1000e6); + vm.prank(cbsIssuer); + uint256 poolId = cbs.createPool(tokenIds, 100, block.timestamp + 365 days); + + vm.prank(investor1); + cbs.purchaseShares(poolId, 25); + + vm.prank(settler); + cbs.depositYield(poolId, 400e6); + + uint256 claimable = cbs.getClaimableYield(poolId, investor1); + assertEq(claimable, 100e6); // 25% of 400 + } + + // --- Helpers --- + + function _mintSecurityCoupons(uint256 count, uint256 faceValue) internal returns (uint256[] memory) { + uint256[] memory tokenIds = new uint256[](count); + bytes32[] memory stores = new bytes32[](0); + + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Security, + transferable: true, + maxResaleCount: 10, + maxPrice: 0, + expiryDate: block.timestamp + 730 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: cbsIssuer, + faceValue: faceValue + }); + + for (uint256 i = 0; i < count; i++) { + vm.prank(admin); + tokenIds[i] = coupon.mint(cbsIssuer, faceValue, config); + } + + return tokenIds; + } +} diff --git a/blockchain/genex-contracts/test/CouponFactory.t.sol b/blockchain/genex-contracts/test/CouponFactory.t.sol new file mode 100644 index 0000000..50dfff0 --- /dev/null +++ b/blockchain/genex-contracts/test/CouponFactory.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/CouponFactory.sol"; +import "../src/Coupon.sol"; +import "../src/Compliance.sol"; +import "../src/interfaces/ICoupon.sol"; + +contract CouponFactoryTest is Test { + CouponFactory factory; + Coupon coupon; + Compliance compliance; + + address admin = address(1); + address issuer = address(2); + address minter = address(3); + + function setUp() public { + vm.startPrank(admin); + + // 部署 Compliance + compliance = new Compliance(); + compliance.initialize(admin); + + // 部署 Coupon + coupon = new Coupon(); + coupon.initialize("Genex Coupon", "GXC", admin); + + // 部署 CouponFactory + factory = new CouponFactory(); + factory.initialize(address(coupon), address(compliance), admin); + + // 授权 + coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory)); + factory.grantRole(keccak256("MINTER_ROLE"), minter); + + // 设置发行方 KYC + compliance.setKycLevel(issuer, 3); + + vm.stopPrank(); + } + + function test_MintBatchUtility() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 5, config); + + assertEq(tokenIds.length, 5); + for (uint256 i = 0; i < 5; i++) { + assertEq(coupon.ownerOf(tokenIds[i]), issuer); + assertEq(coupon.getFaceValue(tokenIds[i]), 100e6); + } + } + + function test_MintBatchSecurity() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _securityConfig(); + uint256[] memory tokenIds = factory.mintBatch(issuer, 1000e6, 3, config); + + assertEq(tokenIds.length, 3); + } + + function test_UtilityMaxPriceExceedsFaceValue() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + config.maxPrice = 200e6; // 超过面值 100e6 + + vm.expectRevert("Utility: maxPrice <= faceValue"); + factory.mintBatch(issuer, 100e6, 1, config); + } + + function test_UtilityMaxExpiry12Months() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + config.expiryDate = block.timestamp + 400 days; + + vm.expectRevert("Utility: max 12 months"); + factory.mintBatch(issuer, 100e6, 1, config); + } + + function test_SecurityRequiresExpiry() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _securityConfig(); + config.expiryDate = 0; + + vm.expectRevert("Security: expiry required"); + factory.mintBatch(issuer, 1000e6, 1, config); + } + + function test_SecurityRequiresKycL3() public { + address lowKycIssuer = address(10); + vm.prank(admin); + compliance.setKycLevel(lowKycIssuer, 1); + + vm.prank(minter); + ICoupon.CouponConfig memory config = _securityConfig(); + + vm.expectRevert("Compliance: insufficient KYC level"); + factory.mintBatch(lowKycIssuer, 1000e6, 1, config); + } + + function test_OnlyMinterCanMint() public { + vm.prank(issuer); + ICoupon.CouponConfig memory config = _utilityConfig(); + + vm.expectRevert(); + factory.mintBatch(issuer, 100e6, 1, config); + } + + function test_ZeroQuantityReverts() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + + vm.expectRevert("CouponFactory: invalid quantity"); + factory.mintBatch(issuer, 100e6, 0, config); + } + + function test_ExcessiveQuantityReverts() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + + vm.expectRevert("CouponFactory: invalid quantity"); + factory.mintBatch(issuer, 100e6, 10001, config); + } + + function test_BatchInfoRecorded() public { + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + factory.mintBatch(issuer, 100e6, 3, config); + + CouponFactory.BatchInfo memory batch = factory.getBatchInfo(0); + assertEq(batch.issuer, issuer); + assertEq(batch.faceValue, 100e6); + assertEq(batch.quantity, 3); + assertEq(batch.tokenIds.length, 3); + } + + function testFuzz_MintBatchQuantity(uint256 quantity) public { + quantity = bound(quantity, 1, 100); + vm.prank(minter); + ICoupon.CouponConfig memory config = _utilityConfig(); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, quantity, config); + assertEq(tokenIds.length, quantity); + } + + // --- Helpers --- + + function _utilityConfig() internal view returns (ICoupon.CouponConfig memory) { + bytes32[] memory stores = new bytes32[](0); + return ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: 100e6, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 100e6 + }); + } + + function _securityConfig() internal view returns (ICoupon.CouponConfig memory) { + bytes32[] memory stores = new bytes32[](0); + return ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Security, + transferable: true, + maxResaleCount: 10, + maxPrice: 0, + expiryDate: block.timestamp + 365 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 1000e6 + }); + } +} diff --git a/blockchain/genex-contracts/test/ExchangeRateOracle.t.sol b/blockchain/genex-contracts/test/ExchangeRateOracle.t.sol new file mode 100644 index 0000000..98bb24f --- /dev/null +++ b/blockchain/genex-contracts/test/ExchangeRateOracle.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/ExchangeRateOracle.sol"; +import "../src/interfaces/IChainlinkPriceFeed.sol"; + +contract MockPriceFeed is IChainlinkPriceFeed { + int256 private _price; + uint256 private _updatedAt; + uint8 private _decimals; + + constructor(int256 price, uint8 dec) { + _price = price; + _updatedAt = block.timestamp; + _decimals = dec; + } + + function setPrice(int256 price) external { + _price = price; + _updatedAt = block.timestamp; + } + + function setUpdatedAt(uint256 ts) external { + _updatedAt = ts; + } + + function latestRoundData() + external + view + override + returns (uint80, int256, uint256, uint256, uint80) + { + return (1, _price, block.timestamp, _updatedAt, 1); + } + + function decimals() external view override returns (uint8) { + return _decimals; + } + + function description() external pure override returns (string memory) { + return "Mock Price Feed"; + } +} + +contract ExchangeRateOracleTest is Test { + ExchangeRateOracle oracle; + MockPriceFeed cnyFeed; + MockPriceFeed jpyFeed; + + address admin = address(1); + + function setUp() public { + vm.startPrank(admin); + + oracle = new ExchangeRateOracle(); + oracle.initialize(900, admin); // 15 min staleness + + cnyFeed = new MockPriceFeed(7_2500_0000, 8); // 7.25 CNY/USD + jpyFeed = new MockPriceFeed(150_0000_0000, 8); // 150 JPY/USD + + oracle.setPriceFeed("CNY", address(cnyFeed)); + oracle.setPriceFeed("JPY", address(jpyFeed)); + + vm.stopPrank(); + } + + function test_GetRateCNY() public view { + uint256 rate = oracle.getRate("CNY"); + assertEq(rate, 7_2500_0000); + } + + function test_GetRateJPY() public view { + uint256 rate = oracle.getRate("JPY"); + assertEq(rate, 150_0000_0000); + } + + function test_UnsupportedCurrencyReverts() public { + vm.expectRevert("Oracle: unsupported currency"); + oracle.getRate("EUR"); + } + + function test_StaleDataReverts() public { + // Ensure block.timestamp is large enough to avoid underflow + vm.warp(2000); + // 设置 CNY feed 过期(20分钟前更新) + cnyFeed.setUpdatedAt(block.timestamp - 1200); + + vm.expectRevert("Oracle: stale price data"); + oracle.getRate("CNY"); + } + + function test_GetRateDetails() public view { + (uint256 rate, uint256 updatedAt, uint8 decimals) = oracle.getRateDetails("CNY"); + assertEq(rate, 7_2500_0000); + assertGt(updatedAt, 0); + assertEq(decimals, 8); + } + + function test_SetMaxStaleness() public { + vm.prank(admin); + oracle.setMaxStaleness(600); // 10 min + assertEq(oracle.maxStaleness(), 600); + } + + function test_RemovePriceFeed() public { + vm.prank(admin); + oracle.removePriceFeed("CNY"); + + vm.expectRevert("Oracle: unsupported currency"); + oracle.getRate("CNY"); + } + + function test_SupportedCurrencyCount() public view { + assertEq(oracle.supportedCurrencyCount(), 2); + } + + function test_InvalidPriceReverts() public { + MockPriceFeed badFeed = new MockPriceFeed(0, 8); + vm.prank(admin); + oracle.setPriceFeed("BAD", address(badFeed)); + + vm.expectRevert("Oracle: invalid price"); + oracle.getRate("BAD"); + } +} diff --git a/blockchain/genex-contracts/test/Governance.t.sol b/blockchain/genex-contracts/test/Governance.t.sol new file mode 100644 index 0000000..cb8025d --- /dev/null +++ b/blockchain/genex-contracts/test/Governance.t.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Governance.sol"; + +contract GovernanceTest is Test { + Governance governance; + + address admin = address(1); + address member1 = address(10); + address member2 = address(11); + address member3 = address(12); + address member4 = address(13); + address member5 = address(14); + address target = address(99); + + function setUp() public { + vm.startPrank(admin); + + address[] memory members = new address[](5); + members[0] = member1; + members[1] = member2; + members[2] = member3; + members[3] = member4; + members[4] = member5; + + governance = new Governance(); + governance.initialize(members, admin); + + vm.stopPrank(); + } + + function test_ProposeCreatesProposal() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "0x1234", "Test proposal"); + + ( + address pTarget, + uint256 executeAfter, + uint256 approvalCount, + bool executed, + bool emergency, + address proposer, + string memory description + ) = governance.getProposal(proposalId); + + assertEq(pTarget, target); + assertEq(approvalCount, 1); // 提案者自动审批 + assertFalse(executed); + assertFalse(emergency); + assertEq(proposer, member1); + assertEq(description, "Test proposal"); + assertEq(executeAfter, block.timestamp + 48 hours); + } + + function test_EmergencyProposalShortTimelock() public { + vm.prank(member1); + uint256 proposalId = governance.proposeEmergency(target, "0x1234", "Emergency"); + + (, uint256 executeAfter,,, bool emergency,,) = governance.getProposal(proposalId); + assertTrue(emergency); + assertEq(executeAfter, block.timestamp + 4 hours); + } + + function test_ApproveAndExecute() public { + // 提案 + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + // member2 和 member3 审批 (共 3 个) + vm.prank(member2); + governance.approve(proposalId); + vm.prank(member3); + governance.approve(proposalId); + + (,, uint256 approvalCount,,,,) = governance.getProposal(proposalId); + assertEq(approvalCount, 3); + + // 快进 48h + vm.warp(block.timestamp + 48 hours); + + // 构造一个可执行的target + // 由于 target 没有代码,call 会成功 + vm.prank(member1); + governance.execute(proposalId); + + (,,, bool executed,,,) = governance.getProposal(proposalId); + assertTrue(executed); + } + + function test_ExecuteNotEnoughApprovalsReverts() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + // 只有 1 个审批(提案者自己) + vm.warp(block.timestamp + 48 hours); + + vm.prank(member1); + vm.expectRevert("Governance: not enough approvals"); + governance.execute(proposalId); + } + + function test_ExecuteTimelockNotExpiredReverts() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + vm.prank(member2); + governance.approve(proposalId); + vm.prank(member3); + governance.approve(proposalId); + + // 不快进时间 + vm.prank(member1); + vm.expectRevert("Governance: timelock not expired"); + governance.execute(proposalId); + } + + function test_DoubleApproveReverts() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + vm.prank(member1); + vm.expectRevert("Governance: already approved"); + governance.approve(proposalId); + } + + function test_CancelByProposer() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + vm.prank(member1); + governance.cancel(proposalId); + + (,,, bool executed,,,) = governance.getProposal(proposalId); + assertTrue(executed); // 标记为已执行以阻止未来执行 + } + + function test_CancelByNonProposerReverts() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + vm.prank(member2); + vm.expectRevert("Governance: not authorized"); + governance.cancel(proposalId); + } + + function test_HasApproved() public { + vm.prank(member1); + uint256 proposalId = governance.propose(target, "", "Test"); + + assertTrue(governance.hasApproved(proposalId, member1)); + assertFalse(governance.hasApproved(proposalId, member2)); + } + + function test_OnlyMultisigCanPropose() public { + address random = address(888); + vm.prank(random); + vm.expectRevert(); + governance.propose(target, "", "Test"); + } + + function test_RecordPreviousImplementation() public { + address proxy = address(100); + address impl = address(101); + + vm.prank(member1); + governance.recordPreviousImplementation(proxy, impl); + + assertEq(governance.previousImplementations(proxy), impl); + } +} diff --git a/blockchain/genex-contracts/test/Integration.t.sol b/blockchain/genex-contracts/test/Integration.t.sol new file mode 100644 index 0000000..0c766b1 --- /dev/null +++ b/blockchain/genex-contracts/test/Integration.t.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/CouponFactory.sol"; +import "../src/Coupon.sol"; +import "../src/Settlement.sol"; +import "../src/Redemption.sol"; +import "../src/Compliance.sol"; +import "../src/Treasury.sol"; +import "../src/interfaces/ICoupon.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDCIntegration is ERC20 { + constructor() ERC20("USD Coin", "USDC") {} + function decimals() public pure override returns (uint8) { return 6; } + function mint(address to, uint256 amount) external { _mint(to, amount); } +} + +/// @title 端到端集成测试 +/// @notice 完整流程: 发行 → 一级市场购买 → 二级市场交易 → 兑付核销 +contract IntegrationTest is Test { + CouponFactory factory; + Coupon coupon; + Settlement settlement; + Redemption redemption; + Compliance compliance; + Treasury treasury; + MockUSDCIntegration usdc; + + address admin = address(1); + address issuer = address(2); // 发行方 + address buyer1 = address(3); // 一级市场买方 + address buyer2 = address(4); // 二级市场买方 + address feeCollector = address(5); // 平台手续费 + + function setUp() public { + vm.startPrank(admin); + + usdc = new MockUSDCIntegration(); + + // 部署所有合约 + compliance = new Compliance(); + compliance.initialize(admin); + + coupon = new Coupon(); + coupon.initialize("Genex Coupon", "GXC", admin); + + factory = new CouponFactory(); + factory.initialize(address(coupon), address(compliance), admin); + + settlement = new Settlement(); + settlement.initialize(address(coupon), address(compliance), address(usdc), admin); + + redemption = new Redemption(); + redemption.initialize(address(coupon), address(compliance), admin); + + treasury = new Treasury(); + treasury.initialize(address(usdc), feeCollector, admin); + + // 角色授权 + coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory)); + coupon.grantRole(keccak256("SETTLER_ROLE"), address(settlement)); + coupon.grantRole(keccak256("SETTLER_ROLE"), address(redemption)); + + // KYC 设置 + compliance.setKycLevel(issuer, 3); // L3 发行方 + compliance.setKycLevel(buyer1, 1); // L1 普通用户 + compliance.setKycLevel(buyer2, 1); // L1 普通用户 + + // 资金准备 + usdc.mint(buyer1, 500_000e6); + usdc.mint(buyer2, 500_000e6); + usdc.mint(issuer, 100_000e6); + + vm.stopPrank(); + + // 全局 approve + vm.prank(buyer1); + usdc.approve(address(settlement), type(uint256).max); + vm.prank(buyer2); + usdc.approve(address(settlement), type(uint256).max); + vm.prank(buyer1); + coupon.setApprovalForAll(address(settlement), true); + vm.prank(buyer2); + coupon.setApprovalForAll(address(settlement), true); + vm.prank(issuer); + coupon.setApprovalForAll(address(settlement), true); + } + + /// @notice 完整 Utility 券生命周期: 铸造 → 一级购买 → 二级交易 → 兑付 + function test_FullUtilityLifecycle() public { + // ========== Step 1: 发行方铸造券 ========== + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: 100e6, // 面值上限 $100 + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 100e6 + }); + + vm.prank(admin); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 5, config); + assertEq(tokenIds.length, 5); + assertEq(coupon.ownerOf(tokenIds[0]), issuer); + + // ========== Step 2: 一级市场 — 发行方卖给 buyer1 ========== + vm.prank(admin); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc)); + + assertEq(coupon.ownerOf(tokenIds[0]), buyer1); + assertEq(usdc.balanceOf(issuer), 100_000e6 + 100e6); + assertEq(coupon.getResaleCount(tokenIds[0]), 1); + + // ========== Step 3: 二级市场 — buyer1 以 $85 卖给 buyer2 ========== + vm.prank(admin); + settlement.executeSwap(tokenIds[0], buyer2, buyer1, 85e6, address(usdc)); + + assertEq(coupon.ownerOf(tokenIds[0]), buyer2); + assertEq(coupon.getResaleCount(tokenIds[0]), 2); + + // ========== Step 4: 兑付 — buyer2 去门店使用 ========== + vm.prank(buyer2); + redemption.redeem(tokenIds[0], keccak256("ANY_STORE")); + + // 券已销毁 + vm.expectRevert(); + coupon.ownerOf(tokenIds[0]); + + // 验证兑付记录 + Redemption.RedemptionRecord memory record = redemption.getRedemption(0); + assertEq(record.consumer, buyer2); + assertEq(record.issuer, issuer); + } + + /// @notice Utility 券价格上限保护 + function test_UtilityPriceCap() public { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: 100e6, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 100e6 + }); + + vm.prank(admin); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config); + + // 尝试以超过面值的价格交易 + vm.prank(admin); + vm.expectRevert("Utility: price exceeds max price"); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 120e6, address(usdc)); + + // 以面值或更低价格交易成功 + vm.prank(admin); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 95e6, address(usdc)); + } + + /// @notice 不可转让券测试 + function test_NonTransferableCoupon() public { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: false, + maxResaleCount: 0, + maxPrice: 50e6, + expiryDate: block.timestamp + 90 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 50e6 + }); + + vm.prank(admin); + uint256[] memory tokenIds = factory.mintBatch(issuer, 50e6, 1, config); + + // 不可转让券不能在二级市场交易 + // With maxResaleCount=0, Settlement's own resale count check (0 < 0 == false) + // fires before the Coupon's _beforeTokenTransfer non-transferable check + vm.prank(admin); + vm.expectRevert("Settlement: max resale count exceeded"); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 50e6, address(usdc)); + } + + /// @notice 合规检查集成 + function test_ComplianceBlocksBlacklistedTrader() public { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: 100e6, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 100e6 + }); + + vm.prank(admin); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config); + + // 将 buyer1 加入 OFAC 黑名单 + vm.prank(admin); + compliance.addToBlacklist(buyer1, "OFAC SDN Match"); + + // 交易被拒绝 + vm.prank(admin); + vm.expectRevert("Compliance: buyer blacklisted"); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc)); + } + + /// @notice 过期券不能交易也不能兑付 + function test_ExpiredCouponBlocked() public { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: 100e6, + expiryDate: block.timestamp + 30 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 100e6 + }); + + vm.prank(admin); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config); + + // 快进到过期 + vm.warp(block.timestamp + 31 days); + + // 交易被拒绝 + vm.prank(admin); + vm.expectRevert("Settlement: coupon expired"); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc)); + + // 兑付也被拒绝 + vm.prank(issuer); + vm.expectRevert("Redemption: coupon expired"); + redemption.redeem(tokenIds[0], keccak256("STORE")); + } + + /// @notice 转售次数用尽后不能再交易 + function test_MaxResaleCountBlocksFurtherTrades() public { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 2, + maxPrice: 100e6, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: issuer, + faceValue: 100e6 + }); + + vm.prank(admin); + uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config); + + // 第1次: issuer → buyer1 + vm.prank(admin); + settlement.executeSwap(tokenIds[0], buyer1, issuer, 100e6, address(usdc)); + + // 第2次: buyer1 → buyer2 + vm.prank(admin); + settlement.executeSwap(tokenIds[0], buyer2, buyer1, 90e6, address(usdc)); + + // 第3次: 应被拒绝(已转售2次,达到上限) + vm.prank(buyer2); + coupon.setApprovalForAll(address(settlement), true); + + vm.prank(admin); + usdc.mint(buyer1, 100e6); + + vm.prank(admin); + vm.expectRevert("Settlement: max resale count exceeded"); + settlement.executeSwap(tokenIds[0], buyer1, buyer2, 80e6, address(usdc)); + + // 但兑付仍然可以 + vm.prank(buyer2); + redemption.redeem(tokenIds[0], keccak256("STORE")); + } +} diff --git a/blockchain/genex-contracts/test/Redemption.t.sol b/blockchain/genex-contracts/test/Redemption.t.sol new file mode 100644 index 0000000..f28d83c --- /dev/null +++ b/blockchain/genex-contracts/test/Redemption.t.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Redemption.sol"; +import "../src/Coupon.sol"; +import "../src/Compliance.sol"; +import "../src/interfaces/ICoupon.sol"; + +contract RedemptionTest is Test { + Redemption redemption; + Coupon coupon; + Compliance compliance; + + address admin = address(1); + address consumer = address(4); + bytes32 storeA = keccak256("STORE_A"); + bytes32 storeB = keccak256("STORE_B"); + + function setUp() public { + vm.startPrank(admin); + + compliance = new Compliance(); + compliance.initialize(admin); + + coupon = new Coupon(); + coupon.initialize("Genex Coupon", "GXC", admin); + coupon.grantRole(keccak256("FACTORY_ROLE"), admin); + + redemption = new Redemption(); + redemption.initialize(address(coupon), address(compliance), admin); + + // Grant Redemption contract the SETTLER_ROLE so it can burn coupons + coupon.grantRole(keccak256("SETTLER_ROLE"), address(redemption)); + + compliance.setKycLevel(consumer, 1); + + vm.stopPrank(); + } + + function test_RedeemSuccess() public { + uint256 tokenId = _mintCoupon(consumer, 100e6, false); + + vm.prank(consumer); + redemption.redeem(tokenId, storeA); + + // 券已销毁 + vm.expectRevert(); + coupon.ownerOf(tokenId); + + // 记录已创建 + Redemption.RedemptionRecord memory record = redemption.getRedemption(0); + assertEq(record.tokenId, tokenId); + assertEq(record.consumer, consumer); + assertEq(record.storeId, storeA); + } + + function test_RedeemExpiredCouponReverts() public { + uint256 tokenId = _mintCoupon(consumer, 100e6, false); + + vm.warp(block.timestamp + 200 days); + + vm.prank(consumer); + vm.expectRevert("Redemption: coupon expired"); + redemption.redeem(tokenId, storeA); + } + + function test_RedeemNotOwnerReverts() public { + uint256 tokenId = _mintCoupon(consumer, 100e6, false); + + vm.prank(admin); + vm.expectRevert("Redemption: not owner"); + redemption.redeem(tokenId, storeA); + } + + function test_RedeemBlacklistedReverts() public { + uint256 tokenId = _mintCoupon(consumer, 100e6, false); + + vm.prank(admin); + compliance.addToBlacklist(consumer, "OFAC"); + + vm.prank(consumer); + vm.expectRevert("Redemption: blacklisted"); + redemption.redeem(tokenId, storeA); + } + + function test_RedeemStoreRestricted() public { + uint256 tokenId = _mintCouponWithStore(consumer, 100e6); + + // 使用不在列表中的门店 + bytes32 storeC = keccak256("STORE_C"); + vm.prank(consumer); + vm.expectRevert("Redemption: store not allowed"); + redemption.redeem(tokenId, storeC); + + // 使用允许的门店 + vm.prank(consumer); + redemption.redeem(tokenId, storeA); + } + + function test_TotalRedemptionsIncrement() public { + uint256 tokenId1 = _mintCoupon(consumer, 100e6, false); + uint256 tokenId2 = _mintCoupon(consumer, 200e6, false); + + vm.startPrank(consumer); + redemption.redeem(tokenId1, storeA); + redemption.redeem(tokenId2, storeA); + vm.stopPrank(); + + assertEq(redemption.totalRedemptions(), 2); + } + + // --- Helpers --- + + function _mintCoupon(address to, uint256 faceValue, bool withStores) internal returns (uint256) { + bytes32[] memory stores; + if (withStores) { + stores = new bytes32[](2); + stores[0] = storeA; + stores[1] = storeB; + } else { + stores = new bytes32[](0); + } + + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: faceValue, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: admin, + faceValue: faceValue + }); + + vm.prank(admin); + return coupon.mint(to, faceValue, config); + } + + function _mintCouponWithStore(address to, uint256 faceValue) internal returns (uint256) { + return _mintCoupon(to, faceValue, true); + } +} diff --git a/blockchain/genex-contracts/test/Settlement.t.sol b/blockchain/genex-contracts/test/Settlement.t.sol new file mode 100644 index 0000000..b1613e2 --- /dev/null +++ b/blockchain/genex-contracts/test/Settlement.t.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Settlement.sol"; +import "../src/Coupon.sol"; +import "../src/Compliance.sol"; +import "../src/interfaces/ICoupon.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDC is ERC20 { + constructor() ERC20("USD Coin", "USDC") { + _mint(msg.sender, 1_000_000e6); + } + + function decimals() public pure override returns (uint8) { + return 6; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract SettlementTest is Test { + Settlement settlement; + Coupon coupon; + Compliance compliance; + MockUSDC usdc; + MockUSDC usdt; + + address admin = address(1); + address buyer = address(4); + address seller = address(5); + + function setUp() public { + vm.startPrank(admin); + + usdc = new MockUSDC(); + usdt = new MockUSDC(); + + compliance = new Compliance(); + compliance.initialize(admin); + + coupon = new Coupon(); + coupon.initialize("Genex Coupon", "GXC", admin); + + settlement = new Settlement(); + settlement.initialize(address(coupon), address(compliance), address(usdc), admin); + + // 授权 + coupon.grantRole(keccak256("FACTORY_ROLE"), admin); + coupon.grantRole(keccak256("SETTLER_ROLE"), address(settlement)); + + // KYC + compliance.setKycLevel(buyer, 1); + compliance.setKycLevel(seller, 1); + + // 给买方 USDC + usdc.mint(buyer, 1000e6); + + vm.stopPrank(); + + // Buyer approve settlement + vm.prank(buyer); + usdc.approve(address(settlement), type(uint256).max); + + // Seller approve coupon to settlement + vm.prank(seller); + coupon.setApprovalForAll(address(settlement), true); + } + + function test_AtomicSwapSuccess() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + vm.prank(admin); + settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); + + assertEq(coupon.ownerOf(tokenId), buyer); + assertEq(usdc.balanceOf(seller), 85e6); + } + + function test_UtilityCannotExceedMaxPrice() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + vm.prank(admin); + vm.expectRevert("Utility: price exceeds max price"); + settlement.executeSwap(tokenId, buyer, seller, 110e6, address(usdc)); + } + + function test_RefundReverseSwap() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + // 先正常交易 + vm.prank(admin); + settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); + + // Seller 退 USDC approve + vm.prank(seller); + usdc.approve(address(settlement), type(uint256).max); + // Buyer approve coupon + vm.prank(buyer); + coupon.setApprovalForAll(address(settlement), true); + + // 退款 + vm.prank(admin); + settlement.executeRefund(tokenId, buyer, seller, 85e6, address(usdc)); + + assertEq(coupon.ownerOf(tokenId), seller); + } + + function test_BlacklistedBuyerReverts() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + vm.prank(admin); + compliance.addToBlacklist(buyer, "OFAC"); + + vm.prank(admin); + vm.expectRevert("Compliance: buyer blacklisted"); + settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); + } + + function test_MaxResaleCountBlocks() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + // 交易 3 次达到上限 + for (uint256 i = 0; i < 3; i++) { + vm.prank(admin); + settlement.executeSwap(tokenId, buyer, seller, 50e6, address(usdc)); + + // Reset: transfer back + fund buyer (skip after last iteration + // because resaleCount == maxResaleCount and P2P transfer would fail) + if (i < 2) { + vm.prank(buyer); + coupon.safeTransferFrom(buyer, seller, tokenId); + vm.prank(admin); + usdc.mint(buyer, 50e6); + } + } + + // 第 4 次应失败 + vm.prank(admin); + vm.expectRevert("Settlement: max resale count exceeded"); + settlement.executeSwap(tokenId, buyer, seller, 50e6, address(usdc)); + } + + function test_ExpiredCouponReverts() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + // 快进到过期 + vm.warp(block.timestamp + 200 days); + + vm.prank(admin); + vm.expectRevert("Settlement: coupon expired"); + settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdc)); + } + + function test_UnsupportedStablecoinReverts() public { + uint256 tokenId = _mintCouponToSeller(100e6); + + vm.prank(admin); + vm.expectRevert("Settlement: unsupported stablecoin"); + settlement.executeSwap(tokenId, buyer, seller, 85e6, address(usdt)); + } + + function test_AddStablecoin() public { + vm.prank(admin); + settlement.addStablecoin(address(usdt)); + assertTrue(settlement.supportedStablecoins(address(usdt))); + } + + // --- Helpers --- + + function _mintCouponToSeller(uint256 faceValue) internal returns (uint256) { + bytes32[] memory stores = new bytes32[](0); + ICoupon.CouponConfig memory config = ICoupon.CouponConfig({ + couponType: ICoupon.CouponType.Utility, + transferable: true, + maxResaleCount: 3, + maxPrice: faceValue, + expiryDate: block.timestamp + 180 days, + minPurchase: 0, + stackable: false, + allowedStores: stores, + issuer: admin, + faceValue: faceValue + }); + + vm.prank(admin); + return coupon.mint(seller, faceValue, config); + } +} diff --git a/blockchain/genex-contracts/test/Treasury.t.sol b/blockchain/genex-contracts/test/Treasury.t.sol new file mode 100644 index 0000000..1a6b29d --- /dev/null +++ b/blockchain/genex-contracts/test/Treasury.t.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Treasury.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockStablecoin is ERC20 { + constructor() ERC20("USD Coin", "USDC") {} + + function decimals() public pure override returns (uint8) { + return 6; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract TreasuryTest is Test { + Treasury treasury; + MockStablecoin usdc; + + address admin = address(1); + address feeCollector = address(2); + address issuer = address(3); + address buyer = address(4); + address seller = address(5); + address claimant1 = address(6); + address claimant2 = address(7); + + function setUp() public { + vm.startPrank(admin); + + usdc = new MockStablecoin(); + treasury = new Treasury(); + treasury.initialize(address(usdc), feeCollector, admin); + + // Fund accounts + usdc.mint(issuer, 100_000e6); + usdc.mint(buyer, 100_000e6); + usdc.mint(seller, 100_000e6); + + vm.stopPrank(); + + // Approvals + vm.prank(issuer); + usdc.approve(address(treasury), type(uint256).max); + vm.prank(buyer); + usdc.approve(address(treasury), type(uint256).max); + vm.prank(seller); + usdc.approve(address(treasury), type(uint256).max); + } + + // === Guarantee Fund === + + function test_DepositGuaranteeFund() public { + vm.prank(issuer); + treasury.depositGuaranteeFund(10000e6); + assertEq(treasury.guaranteeFunds(issuer), 10000e6); + } + + function test_WithdrawGuaranteeFund() public { + vm.prank(issuer); + treasury.depositGuaranteeFund(10000e6); + + vm.prank(admin); + treasury.withdrawGuaranteeFund(issuer, 5000e6); + assertEq(treasury.guaranteeFunds(issuer), 5000e6); + } + + function test_ActivateGuaranteeFund() public { + vm.prank(issuer); + treasury.depositGuaranteeFund(10000e6); + + address[] memory claimants = new address[](2); + claimants[0] = claimant1; + claimants[1] = claimant2; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 3000e6; + amounts[1] = 2000e6; + + vm.prank(admin); + treasury.activateGuaranteeFund(issuer, claimants, amounts); + + assertEq(usdc.balanceOf(claimant1), 3000e6); + assertEq(usdc.balanceOf(claimant2), 2000e6); + assertEq(treasury.guaranteeFunds(issuer), 5000e6); + } + + function test_ActivateInsufficientReverts() public { + vm.prank(issuer); + treasury.depositGuaranteeFund(1000e6); + + address[] memory claimants = new address[](1); + claimants[0] = claimant1; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 5000e6; + + vm.prank(admin); + vm.expectRevert("Treasury: insufficient guarantee fund"); + treasury.activateGuaranteeFund(issuer, claimants, amounts); + } + + // === Escrow === + + function test_EscrowAndRelease() public { + vm.prank(admin); + treasury.escrow(1, buyer, 1000e6); + + assertEq(usdc.balanceOf(address(treasury)), 1000e6); + + vm.prank(admin); + treasury.release(1, seller, 950e6, 50e6); + + assertEq(usdc.balanceOf(seller), 100_000e6 + 950e6); + assertEq(usdc.balanceOf(feeCollector), 50e6); + } + + function test_EscrowRefund() public { + uint256 buyerBefore = usdc.balanceOf(buyer); + + vm.prank(admin); + treasury.escrow(1, buyer, 1000e6); + + vm.prank(admin); + treasury.refundEscrow(1); + + assertEq(usdc.balanceOf(buyer), buyerBefore); + } + + function test_DoubleReleaseReverts() public { + vm.prank(admin); + treasury.escrow(1, buyer, 1000e6); + + vm.prank(admin); + treasury.release(1, seller, 950e6, 50e6); + + vm.prank(admin); + vm.expectRevert("Treasury: already settled"); + treasury.release(1, seller, 950e6, 50e6); + } + + function test_ReleaseExceedsEscrowReverts() public { + vm.prank(admin); + treasury.escrow(1, buyer, 1000e6); + + vm.prank(admin); + vm.expectRevert("Treasury: exceeds escrow"); + treasury.release(1, seller, 1000e6, 100e6); + } + + function test_DuplicateEscrowReverts() public { + vm.prank(admin); + treasury.escrow(1, buyer, 1000e6); + + vm.prank(admin); + vm.expectRevert("Treasury: escrow exists"); + treasury.escrow(1, buyer, 500e6); + } + + // === Sales Revenue === + + function test_FreezeSalesRevenue() public { + vm.prank(issuer); + treasury.freezeSalesRevenue(5000e6); + assertEq(treasury.frozenSalesRevenue(issuer), 5000e6); + } + + function test_ReleaseSalesRevenue() public { + vm.prank(issuer); + treasury.freezeSalesRevenue(5000e6); + + vm.prank(admin); + treasury.releaseSalesRevenue(issuer, 3000e6); + assertEq(treasury.frozenSalesRevenue(issuer), 2000e6); + } + + // === Fee Collector === + + function test_SetPlatformFeeCollector() public { + address newCollector = address(99); + vm.prank(admin); + treasury.setPlatformFeeCollector(newCollector); + assertEq(treasury.platformFeeCollector(), newCollector); + } +}