feat: Genex Chain 区块链完整实现 — cosmos/evm v0.5.1 应用链 + 9合约 + 合规集成
区块链核心 (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 <noreply@anthropic.com>
This commit is contained in:
parent
0c70a030ea
commit
a1293e8445
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -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 小时时间锁
|
||||||
|
- 不可升级的安全红线: 券类型标记、所有权记录、转售计数器
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
build/
|
||||||
|
testnet/
|
||||||
|
.scaffold/
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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 <target>"
|
||||||
|
@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"
|
||||||
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Package main — Genex Chain 节点二进制入口
|
||||||
|
//
|
||||||
|
// genexd 是 Genex Chain 的节点程序,基于 Cosmos SDK + cosmos/evm + CometBFT。
|
||||||
|
// 支持完全 EVM 兼容,提供券 NFT 发行、交易、结算、合规的链上能力。
|
||||||
|
//
|
||||||
|
// 用法:
|
||||||
|
//
|
||||||
|
// genexd init <moniker> --chain-id genex-1
|
||||||
|
// genexd start
|
||||||
|
// genexd keys add <name>
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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 "============================================"
|
||||||
|
|
@ -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 "============================================"
|
||||||
|
|
@ -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 "============================================"
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Dependencies (restore with: forge install)
|
||||||
|
lib/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
out/
|
||||||
|
cache/
|
||||||
|
broadcast/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
|
||||||
|
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue