148 lines
6.2 KiB
Python
148 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
def _has(pattern: str, text: str) -> bool:
|
|
return re.search(pattern, text, re.MULTILINE | re.DOTALL) is not None
|
|
|
|
|
|
def analyze_checkout_contract(source_path: str) -> Dict[str, Any]:
|
|
path = os.path.abspath(source_path)
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
source = fh.read()
|
|
|
|
checks = {
|
|
"has_only_owner_modifier": _has(r"modifier\s+onlyOwner\s*\(", source),
|
|
"owner_set_in_constructor": _has(r"owner\s*=\s*msg\.sender\s*;", source),
|
|
"set_treasury_only_owner": _has(
|
|
r"function\s+setTreasury\s*\([^)]*\)\s*external\s+onlyOwner", source
|
|
),
|
|
"set_token_allowed_only_owner": _has(
|
|
r"function\s+setTokenAllowed\s*\([^)]*\)\s*external\s+onlyOwner", source
|
|
),
|
|
"zero_address_guard_in_constructor": _has(
|
|
r"constructor\s*\([^)]*\)\s*\{\s*require\(\s*_token\s*!=\s*address\(0\)\s*&&\s*_treasury\s*!=\s*address\(0\)",
|
|
source,
|
|
),
|
|
"zero_address_guard_in_setters": _has(
|
|
r"function\s+setTreasury[\s\S]*?require\(\s*_treasury\s*!=\s*address\(0\)",
|
|
source,
|
|
)
|
|
and _has(
|
|
r"function\s+setTokenAllowed[\s\S]*?require\(\s*token\s*!=\s*address\(0\)",
|
|
source,
|
|
),
|
|
"allowed_token_check": _has(r"require\(\s*allowedToken\[token\]", source),
|
|
"amount_non_zero_check": _has(r"require\(\s*amount\s*>\s*0", source),
|
|
"duplicate_order_check": _has(r"require\(\s*!paidOrder\[orderId\]", source),
|
|
"paid_order_written_before_transfer": _has(
|
|
r"paidOrder\[orderId\]\s*=\s*true\s*;\s*require\(IERC20\(token\)\.transferFrom",
|
|
source,
|
|
),
|
|
"emits_order_paid": _has(r"emit\s+OrderPaid\s*\(", source),
|
|
"uses_safe_erc20": _has(r"SafeERC20", source),
|
|
"has_pause_switch": _has(r"\bpaused\b|\bPausable\b|\bwhenNotPaused\b", source),
|
|
"has_rescue_function": _has(
|
|
r"function\s+(rescue|sweep|withdraw|recover)", source
|
|
),
|
|
"binds_plan_amount_onchain": _has(
|
|
r"mapping\s*\(\s*uint256\s*=>[\s\S]*plan|planAmount|require\(\s*amount\s*==",
|
|
source,
|
|
),
|
|
}
|
|
|
|
strengths: List[str] = []
|
|
risks: List[Dict[str, Any]] = []
|
|
|
|
if checks["has_only_owner_modifier"] and checks["set_treasury_only_owner"]:
|
|
strengths.append("关键管理函数受 onlyOwner 保护。")
|
|
if checks["allowed_token_check"]:
|
|
strengths.append("支付代币有 allowlist,避免任意 token 进入收款流程。")
|
|
if checks["duplicate_order_check"] and checks["paid_order_written_before_transfer"]:
|
|
strengths.append("订单去重状态在外部 transferFrom 前写入,能拦住同订单重复支付与典型重入重放。")
|
|
if checks["emits_order_paid"]:
|
|
strengths.append("链上事件 OrderPaid 明确,可作为链下审计与补单的唯一确认源。")
|
|
|
|
if checks["uses_safe_erc20"]:
|
|
strengths.append("使用了 SafeERC20 包装,兼容性更稳。")
|
|
else:
|
|
risks.append(
|
|
{
|
|
"id": "erc20_transfer_assumption",
|
|
"severity": "medium",
|
|
"title": "依赖 IERC20.transferFrom 直接返回 bool",
|
|
"detail": "当前合约直接调用 IERC20.transferFrom。对非标准 ERC20 的兼容性弱于 SafeERC20,建议如未来升级合约时改为 OpenZeppelin SafeERC20。",
|
|
}
|
|
)
|
|
|
|
if not checks["has_pause_switch"]:
|
|
risks.append(
|
|
{
|
|
"id": "no_pause_switch",
|
|
"severity": "medium",
|
|
"title": "缺少紧急暂停开关",
|
|
"detail": "一旦发现代币配置错误、接收地址异常或链上风险,当前合约无法直接暂停 pay。建议升级版合约加入 Pausable。",
|
|
}
|
|
)
|
|
|
|
if not checks["binds_plan_amount_onchain"]:
|
|
risks.append(
|
|
{
|
|
"id": "offchain_price_enforcement",
|
|
"severity": "medium",
|
|
"title": "套餐金额与 planId 绑定主要靠链下校验",
|
|
"detail": "合约事件只记录 planId 与 amount,本身不校验 planId 对应价格。当前依赖后端 intent/confirm 流程校验,后续升级可考虑链上 plan 配置或签名校验。",
|
|
}
|
|
)
|
|
|
|
if not checks["has_rescue_function"]:
|
|
risks.append(
|
|
{
|
|
"id": "no_rescue_function",
|
|
"severity": "low",
|
|
"title": "缺少误转资产救援函数",
|
|
"detail": "当前合约把资金直接转 treasury,不太容易残留余额,但若未来支持更多资产或误转到合约地址,缺少救援路径。",
|
|
}
|
|
)
|
|
|
|
risks.append(
|
|
{
|
|
"id": "single_owner_admin",
|
|
"severity": "medium",
|
|
"title": "owner 为单地址管理模型",
|
|
"detail": "setTreasury 和 setTokenAllowed 由单一 owner 控制。生产建议用多签地址持有 owner,降低单点密钥失窃风险。",
|
|
}
|
|
)
|
|
|
|
runtime_controls = [
|
|
"后端只认链上 OrderPaid 事件,不认前端自报支付成功。",
|
|
"payment event loop 与 confirm loop 已写入 SQLite 审计事件,可做对账与回放。",
|
|
"支持 POLYWEATHER_PAYMENT_RPC_URLS 多 RPC 容灾,单节点故障时可轮换。",
|
|
]
|
|
|
|
recommendations = [
|
|
"生产 owner 建议迁移到多签钱包。",
|
|
"下一版合约优先补 SafeERC20 与 Pausable。",
|
|
"若要进一步收紧授权边界,可把 planId/amount/token 绑定做进链上或 EIP-712 签名校验。",
|
|
"每次合约地址或 allowed token 变更后,都运行静态检查与链上回放脚本。",
|
|
]
|
|
|
|
return {
|
|
"contract_path": path,
|
|
"contract_name": "PolyWeatherCheckout",
|
|
"summary": {
|
|
"strength_count": len(strengths),
|
|
"risk_count": len(risks),
|
|
"highest_severity": "medium" if risks else "none",
|
|
},
|
|
"checks": checks,
|
|
"strengths": strengths,
|
|
"runtime_controls": runtime_controls,
|
|
"risks": risks,
|
|
"recommendations": recommendations,
|
|
}
|
|
|