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:
hailin 2026-02-14 23:40:37 -08:00
parent 0c70a030ea
commit a1293e8445
55 changed files with 8384 additions and 0 deletions

View File

@ -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
```

131
blockchain/README.md Normal file
View File

@ -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 小时时间锁
- 不可升级的安全红线: 券类型标记、所有权记录、转售计数器

View File

@ -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

View File

@ -0,0 +1,9 @@
build/
testnet/
.scaffold/
coverage.out
coverage.html
*.log
.git
.gitignore
*.md

View File

@ -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"]

View File

@ -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"

View File

@ -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"),
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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 链 IDMetaMask 等工具使用)
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 APIEVM 兼容)
# ========================
[json-rpc]
# 是否启用 JSON-RPCMetaMask, 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

View File

@ -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"

View File

@ -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": []
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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 "============================================"

View File

@ -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 "============================================"

View File

@ -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 "============================================"

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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")
}
}

10
blockchain/genex-contracts/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Dependencies (restore with: forge install)
lib/
# Build artifacts
out/
cache/
broadcast/
# Environment
.env

View File

@ -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

View File

@ -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"]

View File

@ -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"
}
}
}

View File

@ -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 }

View File

@ -0,0 +1,2 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/

View File

@ -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");
}
}

View File

@ -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 RuleKYC
/// @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");
}
}
}

View File

@ -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=0to=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);
}
}

View File

@ -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];
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 IDbytes32
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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./ICoupon.sol";
/// @title ICompliance
/// @notice OFACKYCTravel 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;
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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
});
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}