累计价格这个整数会绕回去:长窗口均价字段的位宽边界 图 1
累计价格这个整数会绕回去:长窗口均价字段的位宽边界 · 图 1

想把一段时间的平均价格交给合约用,最省燃料的做法不是存一列历史价,而是存一个只增不减的累加值:每个区块结束时,把”当前价格乘以这个区块到上一个区块之间经过的秒数”加进字段。谁要算均价,就取两个区块上的累加值相减,再除以时间差。这样无论想算五分钟还是五天的均价,都只读两个数、做一次除法。这套办法被大量自动做市和借贷协议采用,它有一个几乎所有使用者都忽略的前提:这个字段是一个整数,位宽有限,而它每块都在变大。

先理解为什么它能省。链上存一次数就要付一次燃料,如果每个区块都写一条完整价格,一年下来光写入费就能压垮小额市场;累加字段永远只占固定大小的存储,写入成本与历史长度无关。好处来自”只加不存”,风险也来自”只加不存”:你无法从这个字段本身判断它是否已经装不下。要估算能装多久,只需看每块的增量。假设字段是无符号整数、上限是二的若干次方,那么”上限除以每块增量”就是它大约能撑多久。价格被记成带固定小数位的定点数时,每块增量近似等于这个定点值乘以出块间隔;也就是说,价格水平越高、小数位越多、出块越快,累加值涨得越快,触顶越早。协议通常会对写入的值做截断(只保留高位若干位)来延长可用时间,代价是精度损失,以及截断规则本身成为必须理解的一层。

触顶之后会发生什么,是这篇的重点。无符号整数在链上不会报错,它会绕回:加满上限之后继续加,值会从头开始。于是累加值序列在某一块突然从接近上限掉到接近零。如果你恰好在绕回点两侧取两个读数相减,用”后来的大数减去先前的小数”这一习惯性写法算出来的是一个巨大的负数或者一个荒谬的正数,均价随之错得离谱。更麻烦的是它不一定天天出现:绕回是累加值层面的事件,与短期行情无关,可能在某个平静的时刻静静发生,然后所有依赖长窗口的协议在同一天读到同一个异常值。

为什么借贷协议的清算最容易被牵连。协议用均价的目的,正是防止有人用一笔即时价格操纵清算。当均价字段绕回,读数可能突然变小或变负,落到不同实现里有几种分支:有的协议会检测到”当前值小于参考值”而放弃使用均价、退回更保守的价格源或直接不执行清算;有的协议把减法写成无符号运算,得到一个天文数字,于是均价高得离谱,反而让本该被清算的仓位安全了很长一段;也有的协议在换算时因为数值过大而 revert,导致依赖它的那条业务路径整段失败。三种分支的共同点是:错误发生在价格语义层,而不是明显的报错层,因此用户很难第一时间察觉。

那么怎么保护自己写的或者自己依赖的长窗口逻辑。第一条防线是主动算位宽预算:把字段类型、截断位数、定点小数位、出块间隔四个参数摆出来,估算从现在到触顶的时间,和协议声明的可信窗口做比较;如果一个协议把窗口拉得接近预算上限,它其实是在用可预期的风险换灵活性。第二条防线是把减法写成有符号判断:任何取两个累加值差的做法,都先检查后一个是否小于前一个;一旦小于,视为可能绕回,改用更短窗口或多协议交叉验证。第三条防线是留一条不依赖均价的旁路:例如协议侧有独立的价格新鲜度检查、有熔断时直接拒绝清算的档位,或者有第二数据源可以对照。三条都不是高深技巧,但必须写在取数那一层,而不是事后靠看板发现。

还有一点常被忽略:绕回不仅影响均价,也会影响”累计值本身被用来记账”的场景。有些协议把手续费、流动性度量、甚至奖励权重也做成累加字段,绕回时同样会造成一次跳变。区别只在于,价格类的跳变会被协议防御挡下或挡歪,而记账类的跳变会直接改变份额或奖励的计算结果。判断一个协议稳不稳,可以看它有没有把这些累加量的位数、增长速率和重置机制写进文档,并在关键路径上留下可读的计数器与校验字段。

如果你要读一个具体池子的累加字段,可以按一个简单流程:先在区块浏览器里连续取若干块的累加值,看它是否单调、平均每块加多少;再用当前值除以每块增量估出剩余块数;最后确认协议读取的是哪两个位置、有没有绕回检查。这三步都不需要付费数据服务,只需要原始链上数据和一个耐心。机制说清楚之后,“长窗口更安全”这句直觉就要改成”长窗口必须在位宽预算内才安全”。本文只做机制说明与风险提示,示例数字均为原理演算,不构成投资建议。

累计价格这个整数会绕回去:长窗口均价字段的位宽边界 图 2
累计价格这个整数会绕回去:长窗口均价字段的位宽边界 · 图 2