6.7 KiB
6.7 KiB
PolyWeather 支付审计与防护说明
最后更新:2026-05-29
1. 当前已落地的防护
链下运行态
- 支付事件扫描与确认循环已把运行态写入 SQLite:
payment_runtime_statepayment_audit_events
- 关键循环现在会记录:
event_loop_startedevent_loop_cycleevent_loop_errorconfirm_loop_startedconfirm_loop_cycleconfirm_loop_error
事件确认边界
- 后端只认链上可验证事件:checkout 合约路径认
OrderPaid,直转路径认对应链上 USDCTransfer。 - 前端提交 intent 不会直接视为支付完成。
confirm_loop会再次按 intent 的chain_id、token_address、收款地址、金额与确认数校验链上交易。- 若确认失败,当前会明确把 intent / transaction 落为失败态,而不是长期停留在
submitted。
当前已显式识别的失败原因包括:
receiver_mismatchsender_mismatchevent_mismatchtoken_mismatchtx_reverted
RPC 多节点容灾
- 支持默认链
POLYWEATHER_PAYMENT_RPC_URLS - 支持多链
POLYWEATHER_PAYMENT_RPC_URLS_BY_CHAIN_JSON - 格式示例:
POLYWEATHER_PAYMENT_RPC_URLS=https://polygon-rpc.com,https://polygon-bor-rpc.publicnode.com
POLYWEATHER_PAYMENT_RPC_URLS_BY_CHAIN_JSON={"137":["https://polygon-rpc.com","https://polygon-bor-rpc.publicnode.com"],"1":["https://ethereum-rpc.example"]}
- 启动时按顺序探活。
- 当前节点断连或收据查询失败时,会自动切换到下一个可用 RPC。
- 多链支付确认不会用默认链硬查交易;每笔 intent 会按自己的
chain_id选择 RPC。
事件重放
- 已提供脚本:
用途:
- 审计某个区块范围内的
OrderPaid - 事后补查漏单
- 排查 RPC 抖动导致的监听遗漏
命令示例:
python scripts/replay_payment_events.py --from-block 10000000 --to-block 10001000
运行态检查
- 已提供接口:
GET /api/payments/runtime
可查看:
- checkout 配置摘要
- 当前活跃 RPC
- 候选 RPC 列表
- event loop 最新状态
- 最近审计事件
Ops 事故单
现在 /ops 已提供单独的支付异常单列表,默认展示:
payment_intent_failed
支持:
- 按
reason过滤 - 标记已处理
这让下面这类事故不再需要翻日志定位:
- 已付款但未开通
- 打到旧收款地址
- 交易事件不匹配
- 用户在钱包默认 Ethereum 网络付款,但旧系统只按 Polygon intent 查账
2. 当前支付链路边界
当前支持两类链路:
- Polygon checkout 合约
- 前端钱包支付会先切到 Polygon。
- 后端创建 intent 后给出合约
tx_payload。 - 确认时校验
OrderPaid(orderId, payer, planId, amount, token)。
- Ethereum 主网 USDC 直转
- 前端展示 Ethereum 网络、USDC 合约、收款钱包和金额。
- 用户提交 tx hash 后,后端按
intent.chain_id=1查询 Ethereum RPC。 - 确认时校验 USDC
Transfer(from, to, amount)的to、token_address和金额。 - 这条链路不依赖 Polygon checkout 合约,适合处理“用户钱包默认网络付款”的真实行为。
3. 当前合约的授权边界
合约源码:
当前边界:
owner
- 可执行:
setTreasurysetTokenAllowed
- 普通用户
- 只能调用:
pay(orderId, planId, amount, token)
- 代币边界
- 只有
allowedToken[token] == true的 token 可支付
- 订单边界
- 同一个
orderId只能成功支付一次
4. 重入与重复支付判断
当前合约的 pay 逻辑顺序是:
- 检查 token allowlist
- 检查
amount > 0 - 检查
paidOrder[orderId] == false - 先写入
paidOrder[orderId] = true - 再执行
transferFrom - 发出
OrderPaid
这意味着:
- 同一
orderId的重复支付会被拦住 - 典型“转账外部调用后再回调重复执行同订单”的路径会被
paidOrder状态挡住
但要注意:
- 当前合约没有
Pausable - 当前合约没有
SafeERC20 - 当前合约没有在链上校验
planId -> amount
所以它属于:
- 最小可用支付合约
- 不是“全功能强防护合约”
5. 当前静态审计结论
已提供脚本:
命令:
python scripts/check_payment_contract_security.py
输出会检查这些项目:
- 是否有
onlyOwner setTreasury/setTokenAllowed是否受 owner 保护- constructor / setter 是否检查零地址
- 是否校验 allowlist
- 是否校验
amount > 0 - 是否校验重复订单
- 是否在
transferFrom前写入paidOrder - 是否有 pause 开关
- 是否使用 SafeERC20
- 是否在链上绑定套餐价格
6. 当前主要剩余风险
- 单地址 owner
- 建议把
owner迁移到多签钱包
- 无暂停开关
- 发现紧急问题时,无法直接暂停
pay
- 金额校验主要在链下
- 当前
planId / amount / token绑定主要靠后端 intent 和确认逻辑
- ERC20 兼容性假设
- 当前使用
IERC20.transferFrom - 升级版合约更建议改为 OpenZeppelin
SafeERC20
7. 推荐操作
每次支付配置变更后
执行:
python scripts/check_payment_contract_security.py
python scripts/replay_payment_events.py --from-block <from> --to-block <to>
线上巡检
执行:
curl http://127.0.0.1:8000/api/payments/runtime
重点看:
rpc.active_rpc_urlrpc.configured_rpc_countevent_loop_state.last_scanned_blockrecent_audit_events
如果你在 /ops 或脚本里看到:
receiver_mismatch
其含义通常不是“缓存没刷新”,而是:
- 用户这笔交易的
to地址不是当前生产收款合约 - 常见原因是旧页面、旧 deployment、旧钱包会话,或历史收款地址仍被命中
此时应优先做:
- 确认链上真实
to地址 - 确认当前
/api/payments/config返回的receiver_contract - 如确已收款,再走人工恢复或补开订阅
按邮箱恢复最近支付
已提供脚本:
命令:
docker compose exec polyweather_web python scripts/reconcile_subscription_by_email.py --email user@example.com
适用场景:
- 用户声称已付费但未开通
- 需要快速确认最近一笔 intent 是否能自动恢复
8. 下一版合约建议
如果后续升级合约,优先级建议:
Ownable-> 多签 ownerSafeERC20Pausable- 链上 plan/amount/token 绑定
- 必要时增加 rescue/sweep 能力