Files
2026-05-29 01:57:42 +08:00

6.7 KiB

PolyWeather 支付审计与防护说明

最后更新:2026-05-29

1. 当前已落地的防护

链下运行态

  • 支付事件扫描与确认循环已把运行态写入 SQLite:
    • payment_runtime_state
    • payment_audit_events
  • 关键循环现在会记录:
    • event_loop_started
    • event_loop_cycle
    • event_loop_error
    • confirm_loop_started
    • confirm_loop_cycle
    • confirm_loop_error

事件确认边界

  • 后端只认链上可验证事件:checkout 合约路径认 OrderPaid,直转路径认对应链上 USDC Transfer
  • 前端提交 intent 不会直接视为支付完成。
  • confirm_loop 会再次按 intent 的 chain_idtoken_address、收款地址、金额与确认数校验链上交易。
  • 若确认失败,当前会明确把 intent / transaction 落为失败态,而不是长期停留在 submitted

当前已显式识别的失败原因包括:

  • receiver_mismatch
  • sender_mismatch
  • event_mismatch
  • token_mismatch
  • tx_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. 当前支付链路边界

当前支持两类链路:

  1. Polygon checkout 合约
  • 前端钱包支付会先切到 Polygon。
  • 后端创建 intent 后给出合约 tx_payload
  • 确认时校验 OrderPaid(orderId, payer, planId, amount, token)
  1. Ethereum 主网 USDC 直转
  • 前端展示 Ethereum 网络、USDC 合约、收款钱包和金额。
  • 用户提交 tx hash 后,后端按 intent.chain_id=1 查询 Ethereum RPC。
  • 确认时校验 USDC Transfer(from, to, amount)totoken_address 和金额。
  • 这条链路不依赖 Polygon checkout 合约,适合处理“用户钱包默认网络付款”的真实行为。

3. 当前合约的授权边界

合约源码:

当前边界:

  1. owner
  • 可执行:
    • setTreasury
    • setTokenAllowed
  1. 普通用户
  • 只能调用:
    • pay(orderId, planId, amount, token)
  1. 代币边界
  • 只有 allowedToken[token] == true 的 token 可支付
  1. 订单边界
  • 同一个 orderId 只能成功支付一次

4. 重入与重复支付判断

当前合约的 pay 逻辑顺序是:

  1. 检查 token allowlist
  2. 检查 amount > 0
  3. 检查 paidOrder[orderId] == false
  4. 先写入 paidOrder[orderId] = true
  5. 再执行 transferFrom
  6. 发出 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. 当前主要剩余风险

  1. 单地址 owner
  • 建议把 owner 迁移到多签钱包
  1. 无暂停开关
  • 发现紧急问题时,无法直接暂停 pay
  1. 金额校验主要在链下
  • 当前 planId / amount / token 绑定主要靠后端 intent 和确认逻辑
  1. 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_url
  • rpc.configured_rpc_count
  • event_loop_state.last_scanned_block
  • recent_audit_events

如果你在 /ops 或脚本里看到:

  • receiver_mismatch

其含义通常不是“缓存没刷新”,而是:

  • 用户这笔交易的 to 地址不是当前生产收款合约
  • 常见原因是旧页面、旧 deployment、旧钱包会话,或历史收款地址仍被命中

此时应优先做:

  1. 确认链上真实 to 地址
  2. 确认当前 /api/payments/config 返回的 receiver_contract
  3. 如确已收款,再走人工恢复或补开订阅

按邮箱恢复最近支付

已提供脚本:

命令:

docker compose exec polyweather_web python scripts/reconcile_subscription_by_email.py --email user@example.com

适用场景:

  • 用户声称已付费但未开通
  • 需要快速确认最近一笔 intent 是否能自动恢复

8. 下一版合约建议

如果后续升级合约,优先级建议:

  1. Ownable -> 多签 owner
  2. SafeERC20
  3. Pausable
  4. 链上 plan/amount/token 绑定
  5. 必要时增加 rescue/sweep 能力