这次工作的目标,是让两个独立钱包在 MOTO 主网新币开盘时执行同一套买入规则:监听曲线合约的待打包发币交易和 TokenCreated 日志,跳过名称含 test 或“测试”的代币,首个合格币与白名单 dev 币按预设金额买入。最终脚本完成了实现与部署,但没有发生买入;应用户要求,服务器上的服务现已停止并禁用自动启动。
这份复盘记录实际发生的事,以及下一次上线前必须验证的边界。它不包含私钥、RPC 凭据、服务器地址或完整买入钱包地址。
目标、实现与最终状态
脚本用一个 Python 文件承载监听、解码、报价、签名、广播、回执跟踪和恢复逻辑;依赖、环境变量示例和测试另列文件。两个钱包共享发币信号,各自维护 nonce 和状态文件。当前确认的买入规则是:每个钱包对普通首币买 0.2 ETH,再对首个不同的白名单 dev 币买 0.2 ETH;如果首币就是白名单 dev,只买这一个币的 0.3 ETH,随后停止。普通路径每钱包最多买入 0.4 ETH;两个钱包都执行时,合计最多买入 0.8 ETH。
主网曲线代理为 0xb65b67e986d7b097652920d72202f2c78319bc2c,关联的建池合约为 0x81c9cbc47d700da1777abd831d8da3f526dfae24。脚本启动时核对 chainId、代理 implementation 的运行时代码哈希、建池合约指针、代币实现和曲线参数。最终核查显示服务为 inactive/dead、已 disabled,主进程 PID 为 0,服务器上没有该脚本进程。已核查日志中没有买入广播或确认记录;这不是一次已经验证过实盘成交的上线。
过程与关键转折
| 阶段 | 事实 | 影响 |
|---|---|---|
| 测试网研究 | 从 Sepolia 配置、已验证源码和历史发币与买入交易确认合约接口、CREATE2 代币地址及曲线报价。 | 有了离线回放依据,但不能假设主网事件 ABI 完全相同。 |
| 初版部署 | 脚本部署为 Ubuntu 用户服务,最初只配置一个钱包,后来改为两个钱包共享监听、各自管理交易状态。 | 双钱包下单可以并发,状态文件不会互相覆盖。 |
| 主网部署出现 | 代理与建池合约均有代码,曲线代理指向新的主网 implementation。 | 运行时代码哈希与 Sepolia 版本不同,旧版脚本在校验处退出并反复重启。 |
| 源码复核 | 主网 implementation 的已验证源码保留了 buy(address,uint256) 与原曲线计算,但 TokenCreated 增加了 indexed quoteToken;初始化还增加了地址校验和事件。 | 更新哈希还不够,必须同时更新事件 topic;否则会漏掉新币创建日志。 |
| 余额与规格纠正 | 初版把金额写成普通首币 0.2、后续 dev 0.3;首币为 dev 后还安排第二笔 0.15。余额门槛又按更高的取消交易 gas 上限预留。 | 规则偏离原要求,形成每钱包 0.654 ETH 的过高启动门槛。 |
| 停服与修正 | 按用户要求停止并禁用服务;离线修正买入规则、余额计算和一个 nonce 初始化错误,服务器文件与配置已同步。 | 18 个测试通过,但服务保持停用,未重新开始监听或交易。 |
问题一:金额规则没有先写成可检验的决策表
最严重的偏差是需求解释错误。原意是“普通首币 0.2 ETH + 后续 dev 币 0.2 ETH;如果首币就是 dev,则买 0.3 ETH 后停止”。旧逻辑把后续 dev 金额写为 0.3 ETH,还在 dev 首币之后保留一笔 0.15 ETH。这个错误会直接改变真实支出,不能只靠叙述性说明或一个普通路径测试发现。
后来又出现“总额不到 0.5 ETH”究竟指单钱包还是两钱包合计的歧义。最终按用户再次明确的单钱包规则修正:普通路径每钱包 0.4 ETH;两钱包同额执行,合计上限 0.8 ETH。下次应在签名和部署前固定一张表,逐行列出“首币是否白名单 dev、是否存在第二笔、每钱包金额、两钱包合计金额”,并让测试直接断言每个分支不会多买。
问题二:主网 ABI 变化与代码哈希变化必须一起处理
主网代理所指 implementation 的运行时代码哈希为 0x390f270a27bca54bd8c81633a36a7dcba960a8849ab035423f85a81e965249f8,与脚本原先审查的 Sepolia 哈希不同。哈希闸门让服务在确认新版本前停止交易,这个保护起了作用;但服务的自动重启把同一错误重复写入日志,未及时给出一次清晰的部署阻断结论。
源码比较还发现了更隐蔽的问题:主网 TokenCreated 的最后一个参数是 indexed quoteToken,因此事件签名从五个参数变为六个参数。即使直接放行新代码哈希,旧 topic 订阅也收不到主网新币事件。修正时同步更新了哈希与 topic,核对了代理指向、motoFactory()、代币实现、手续费和曲线参数;当时查询的最近 5,000 个区块没有该事件。今后每次合约部署或升级都应比较运行时代码、ABI 事件和关键 getter,不能只替换一个哈希常量。
问题三:把买入金额、gas 上限和实际花费混成了一个数
旧版的每钱包 0.654 ETH 门槛来自错误的 0.5 ETH 买入计划,加上两笔 350000 × 220 gwei 的取消交易上限预留(0.154 ETH)。maxFeePerGas 是交易可支付的上限,不等于最终实际支付的 gas;取消上限也不该成为两笔初次买入的统一启动门槛。
纠正后,普通路径每钱包最多买入 0.4 ETH;两笔初次交易各以 350000 × 150 gwei 估算上限,共预留 0.105 ETH,因此当前启动余额门槛是每钱包 0.505 ETH。首币为 dev 时只计划一笔 0.3 ETH。重试或取消若使用更高 gas 上限,仍可能需要额外余额;这应在重试路径中单独检查和告警,而不是把最高取消价当成已发生的买入支出。
问题四:测试覆盖了签名,却漏了“余额达标后的空状态启动”
在补充余额边界测试时,发现原 nonce 初始化使用 max(链上 nonce, *已记录 nonce)。当钱包没有任何已记录交易时,展开后的调用只剩一个整数,Python 会把它当作可迭代对象,抛出 TypeError。此前服务一直被合约校验或余额门槛拦住,这条路径没有被触发;若在真正开盘前刚好充值完成,脚本仍会启动失败。现在已修复,并让测试覆盖“余额差 1 wei”和“恰好达标”的边界。离线测试最终为 18/18 通过,但仍不能替代完整的链上模拟与实际广播验证。
还有哪些技术点值得优化
- 把下单规则做成状态机和金额不变量。 对普通首币、dev 首币、测试名、pending 替换、交易失败、重启恢复逐一建模;每钱包累计
value不得超过对应分支上限。双钱包的合计上限也应单独展示,避免“单钱包”与“总预算”混淆。 - 建立主网版本验收流程。 对每次代理 implementation 变动,自动读取链上代码、已验证源码和 ABI,核对事件 topic、买入函数、初始化参数、代币实现与建池指针。版本不符时保持停止,并发出一条明确告警,避免 5 秒一次的失败重启。
- 在不开单的影子模式验证数据通路。 先只订阅 pending、
TokenCreated、新区块和历史补查,记录事件到达顺序与延迟,再核对 CREATE2 预测和链上事件;完整通过后才允许签名广播。私有交易可能不进入公开 pending,仍需 mined log 兜底。“第一个”必须明确按看到的 pending 顺序还是最终链上顺序定义。 - 把资金检查分成初次广播、重试、取消三层。 初次检查以买入
value加初次交易 gas 上限为准;替换交易和取消交易在执行前重新估算可用余额。对 base fee、RPC 接受时间和交易是否因余额不足被拒绝分别记录指标。 - 测量端到端延迟而不是删掉恢复机制。 记录“发现 pending → 创建日志到达 → 签名完成 → 首个 RPC 接受”四个时间点;双钱包应分别记录。签名前的报价复用、并行广播和持久化状态都有取舍,不能为了省几毫秒移除交易恢复所需的落盘记录。
- 强化上线测试和日志卫生。 增加主网 ABI 样本、双钱包并发、无状态 nonce、升级事件、断线补查和交易替换测试。HTTP 客户端曾把带访问凭据的 RPC URL 写入旧日志,后来才压低日志级别;以后应在首次部署前检查脱敏,并轮换可能出现在旧日志中的 RPC 令牌。
- 重新审视 88% 滑点。 当前设置容许成交代币数低至预计值的约 12%。这是为抢速度作出的高风险取舍,应以独立配置和实测结果审查,不应因为脚本跑通就默认继续沿用。
下一次启动前的最小验收
确认每钱包与两钱包合计预算、dev 白名单、滑点和 gas 上限;核对当前代理实现及事件 ABI;做一次不广播交易的主网监听回放;检查两个钱包余额、nonce 和状态文件;最后再由操作者明确决定是否启用服务。最后一次服务器核查时,该服务已停用、没有运行中的买入进程。这次工作的经验是:合约身份、事件格式、金额规则、资金门槛和空状态启动必须分别验证,任何一项通过都不能代替其他项。