池子余额和账本对不上:对账函数与它防的那类事故 图 1
池子余额和账本对不上:对账函数与它防的那类事故 · 图 1

在链上浏览器里读池子合约源码,你会看到两个几乎所有普通用户这辈子都不会调用、却值得每个认真做市的人理解的函数:一个把池子的实际代币余额强行写回记账储备,一个把超出记账范围的多余代币取走。它们像是消防栓——装在墙里不是因为经常着火,而是因为火烧起来时你只有它。这篇文章讲清「余额与账面不一致」这种状态到底怎么产生、这两个函数怎么收尾、以及为什么它们在某些情形下反而是攻击者的台阶。

先说这种错位为什么会存在。普通用户的心智模型是:池子余额等于池子储备,两者永远一致。但机制上「储备」是合约存储里的一组变量,「余额」是代币合约账本上池子地址名下的实际数量,两者只在兑换和存撤流动性的流程里被同步。中间任何一条路径让代币单方面流进或流出池子而不走兑换流程,两者就开始分叉。常见的分叉来源有四类:一是某些代币在转账时附带额外逻辑——弹性供应量代币做再平衡时会给所有地址(包括池子)加减余额,自带转账税的代币让「转进来一百个、到账九十七个」成为常态;二是有人直接把代币空投或误转到池子地址;三是代币协议升级、冻结销毁之类的管理操作直接动了池子名下余额;四是记账变量的数值上限被撑破——储备变量用固定位宽存储,极大余额会溢出导致交易全部 revert,这类事故需要另一种取出方式。

分叉的实际伤害取决于方向。余额多于储备时,池子照常交易,但那部分「幽灵余额」无人认领、也参与不了定价,只是躺着;如果错位的量很大、逼近位宽上限,兑换会开始报错,整个池子停摆。余额少于储备时更危险:定价公式还按老储备报价,但真金白银已经变少,套利者会优先收割这个差价——每薅一笔,储备虚高被修正一点,正常交易者在错位的尾部多付成本。所以这类修复函数的设计哲学是:把修正的按钮做成公共的,谁都能按,按下去之后账本向现实低头,避免错位长期存在或被悄悄收割。

修复函数自身也有代价,这是最容易误读的部分。把余额写回储备的那一刻,池子的报价瞬间改变——对依赖价格连续性的协议(预言机读取、清算判断),这是一次「人为的价格跳跃」。也正因如此,攻击模式里有一种「借道对账」的打法:利用某些代币的特性(转账附带改余额、或先给池子打一笔巨款再触发某种同步逻辑)让余额暴涨,骗过把「储备」当价格依据的合约,让它们以为发生了大额交易或价格移动。防御端的演进就是:预言机改用累计价格变量绕开瞬时储备,把「读储备」和「用储备定价」隔离开。换句话说,对账函数本身不是漏洞,把可变储备当可信价格源才是问题。

普通用户在什么时候需要意识到它们的存在?三个场景。第一个,你发现某个池子交易频繁失败而行情正常,且池子的储备数字和浏览器查到的实际余额对不上——大概率是错位事件,看协议官方频道有没有公告,别急着自己「帮忙」调用不了解的函数。第二个,你做市的是一个含特殊代币(弹性供应、带税、有管理功能)的池子,上仓前应该读一眼代币合约有没有再平衡或管理接口,这些接口决定了这个池子有没有额外的结构性磨损。第三个,遇到有人公开喊话「帮我调一下这个函数」的社群求助,先警惕:这类维护函数不需要普通用户代劳,让你调它的多半是钓鱼脚本。

最后放一份小清单。评估一个池子的「账本健康度」,依次看:池子储备与实际余额当前是否一致(浏览器两个数字对一下);池内代币里有没有带再平衡、转账税、黑名单、管理暂停等特殊逻辑的资产;协议有没有公开的事件流水记录每一次同步;价格消费方用的是瞬时储备还是累计均价。四项都清晰的池子,这类事故的尾部风险就小得多。协议实现与代币版本会更新,本文提到的函数名与机制细节请以对应合约源码为准。做市有资产损失风险,本文只做机制说明,不构成投资建议。

池子余额和账本对不上:对账函数与它防的那类事故 图 2
池子余额和账本对不上:对账函数与它防的那类事故 · 图 2