Hook 与防狙击

Uniswap v4 Hook 是一个由 PoolManager 在一次兑换的特定时刻调用的合约。StockFunHook 当前的实现合约使用了六个权限:beforeInitialize、beforeAddLiquidity、beforeSwap、afterSwap,以及让它能够抽成的两个 delta 权限。自 2026-10-02 起,它的地址携带全部 14 个 v4 权限;它不使用的回调会直接放行。

地址编码了权限

v4 读取 Hook 地址的低位比特,以确定何时调用它。因此,地址不是选出来的:它是通过 CREATE2 挖掘出来的,直到出现一个比特位与所声明权限相匹配的值。

自 2026-10-02 起,挖掘的是一个代理合约的地址,全部 14 个权限比特位都被置位。升级会替换这个地址背后的实现合约而不移动地址,因此它永远不需要重新挖掘,而以后的实现合约可以使用任何回调。当前的实现合约让它不使用的回调直接放行:它们返回自己的选择器,并在需要 delta 的地方返回零。代理合约的初始化会检查其地址是否携带每一个比特位。

实际后果:挖掘出的地址取决于代理合约的字节码和其构造函数参数,而后者包含实现合约的地址。新的部署必须重新挖掘。这也是 foundry.toml 设置 bytecode_hash = "none" 的原因——没有它,挖出的地址就不再与部署的合约相匹配。

beforeAddLiquidity 权限于 2026-10-01 加入,它改变了这些比特位本身:地址已重新挖掘,在该日期之前进行的任何部署都已过时。2026-10-02 的代理合约再次改变了它们:在该日期之前进行的任何部署同样已过时。

收取费用

在买入时,费用在 beforeSwap 中收取,在兑换发生之前从流入的 ETH 中扣除。Hook 从 PoolManager 中取出其份额,并返回一个 delta,把这笔费用记在买家名下。StockFun 路由器和流动性锁定合约会在兑换之前把买家的 ETH 支付给 PoolManager,因此它们的买入从不动用 PoolManager 已经持有的 ETH。

其他任何 v4 路由器都支付同样的税率。但如果一个路由器在兑换之后才结算买家的 ETH,就像 v4-periphery 的 V4Router 在其默认编码下所做的那样,这笔税就会从 PoolManager 已经持有的 ETH 中扣除,而当税额超过这部分 ETH 时,它的买入就会回滚。这可能发生在持有 ETH 很少的 PoolManager 上,例如测试网上的 PoolManager。集成方应当像官方路由器那样,在兑换之前先支付买家的 ETH。卖出不受影响。2026-10-01 的安全流水线将这一限制写入了文档。改为以 PoolManager claims 的形式收取税,可以为所有路由器解除这一限制,但会让国库的 2 % 从在交易期间支付,变为先记账、之后再支付:2026-10-05,所有者决定保持目前收取税的方式。

在卖出时,费用在 afterSwap 中收取,在金额确定之后从流出的 ETH 中扣除。

在这两种情况下,Hook 都会立即拆分:国库、创建者、团队、回购。只有国库这一项在交易期间转出,发送给该市场的金库。其余三项记在 Hook 上,并在任何交易之外支付。

自 2026-10-05 起,拒收国库那一项的金库不再让交易失败:Hook 会把这笔数额作为欠该金库的款项保留(treasuryOwed,事件 TreasuryOwed),每一笔买入和卖出照常进行。任何人都可以通过 payTreasury 向该金库支付这笔欠款;只要金库仍然拒收,这个调用就会失败,欠款继续保留;Keeper 每个循环都会尝试。防狙击的超出部分遵循同样的规则。在此之前,这次发送会显式失败:一个无法接收的金库(例如在一次有缺陷的升级之后)会让其市场的每一笔交易都失败,卖出也不例外。

创建者份额采用拉取式:它按市场分别在 Hook 上累积,创建者可以在任何时候领取,每个市场一笔交易(claimCreatorFees)。自 2026-10-05 起,自身无法接收 ETH 的创建者(例如一个没有接收途径的合约)可以领取到另一个地址(claimCreatorFeesTo);只有创建者本人可以这样做。

自 2026-10-01 起,团队份额和回购份额也以同样的方式记账,各有一个余额,分别由 claimTeamFees() 和 claimBuybackFees() 支付。任何人都可以调用它们;它们只向领取时工厂合约所指定的团队钱包和回购钱包付款。BuybackBurner 在每次销毁开始时自行拉取其余额。拒收 ETH 的钱包只会推迟它自己的款项:它的领取会失败,余额则留在 Hook 上。

在此之前,这两项份额在交易期间发送,发送失败时以托管作为兜底。2026-09-29 的安全审计表明,一个接受了这笔发送、随后调用 PoolManager 的钱包,可以让每一个池子的交易都停止。托管已被移除。

Hook 在两次交易之间持有的东西都是欠某一方的:创建者的余额、团队和回购的余额,以及欠各金库的款项。除 PoolManager 之外的任何人发给它的 ETH 会被单独计为误入资金(strayEth)。自 2026-10-05 起,StockFun 的所有者可以把误入资金转出(rescue):至多这一数额的 ETH,以及任何代币,因为 Hook 从不持有代币。Hook 所欠的任何款项都无法通过这种方式离开。不经调用被强行转入的 ETH 不计入其中,只能等待一次升级。

只有锁定合约能添加流动性

自 2026-10-01 起,beforeAddLiquidity 拒绝向 StockFun 池子添加流动性的一切操作,只有流动性锁定合约的添加除外。

紧挨当前价格放置的头寸相当于一张限价单:交易者的兑换穿过它并将其转换,由交易者支付税,而该头寸的所有者添加和移除它时,却从不支付 5 %,在前十个区块中也不支付防狙击税。2026-09-29 的安全审计复现了这一点。这些池子默认不收取 LP 手续费,因此任何正当用途都不需要第三方头寸。

锁定合约自身的手续费收取,其流动性 delta 为零,走的是移除流动性的路径,而当前的实现合约对这条路径直接放行:它们不受影响。终止模式的回收同样不受影响,它通过同一条路径取出头寸:参见双头寸发射。

递减式防狙击

v4 池在初始化的那一刻即开始运行。如果没有保护,创建后的最初几个区块就会被机器人抢占。自 2026-09-28 起,这些区块需支付更高的税,税率每过一个区块都会下降,买入和卖出同样适用。在默认设置下:

发射后的区块 税
1 80 %
2 至 10 72、64、56、48、40、32、24、16、8 %
11+ 正常,5 %

在开盘区块买入的机器人会一次性支付 80 %:狙击会亏钱。

有三点值得解释。

创建者在发射时的买入不需要身份检查。LiquidityLock 唯一会执行的兑换,就是在它自己的创建回调中进行的、可选的创建者买入。因此,“调用者是锁定合约”就意味着“我们处于创建交易之中”,而这笔买入支付正常的 5 %。

超出部分有自己的去向。正常的 5 % 保持其通常的拆分。超出的部分进入该市场的国库,并因此在下一次空投时分发给持有者。在 $STOCKFUN 市场上,它进入团队在 Hook 上的余额,由 claimTeamFees() 支付。

白名单需要身份识别,而这正是微妙之处。Hook 看到的是路由器,而不是买家。因此,StockFun 路由器会在 hook data 中携带其调用者的地址,而 Hook 只有在调用者是官方路由器时才信任该字段;官方路由器即记录在工厂合约中的那一个,所有者可以随时更改它(于 2026-09-28 接受)。本协议中任何地方都没有使用 tx.origin。

已记录的限制:白名单地址如果在前十个区块内通过第三方聚合器路由交易,则不会获得豁免。

白名单

由市场的创建者设定。名单上的地址在防狙击区块期间支付正常的 5 %。在 2026-09-28 之前,这份名单由所有者持有并由所有市场共用,正是为了防止它变成内部人的优势;如今,创建者可以豁免自己的钱包。已于 2026-09-28 审定:名单在创建交易中确定,公开、不可更改,上限默认为 20 个地址。

$STOCKFUN 市场

递减税同样适用。$STOCKFUN 没有外部创建者:其超出部分进入团队可领取的费用,其白名单由所有者在发射时设定。

设置

自 2026-10-05 起,本页的每一个数值都是 StockFun 所有者的一项设置,通过 Hook 上的 setTaxSettings 更改:税,默认为 5 %;其各项份额,已发射市场上为 2 / 2 / 0.5,$STOCKFUN 上为 2 / 2.5,其余归回购;防狙击在开盘区块的税,80 %;其每个区块的降幅,8 个点;其持续的区块数(含创建区块),10;以及最大的白名单,20 个地址。

变更从下一笔兑换起适用于每个池子,包括仍处于防狙击区块内的池子:递减按当时有效的设置、从该池子的发射区块起计算。无论设置如何,防狙击收取的税都从不低于正常的税。该设置函数会拒绝超过 100 % 的费率,以及各项份额之和超过税的费率结构。白名单上限在创建市场时检查:已经设定的名单保持不变。

升级

自 2026-10-02 起,StockFun 的所有者可以升级 Hook,并立即生效。本页所述的税、其拆分、防狙击和白名单,都属于当前的实现合约。升级会保留 Hook 的地址,并且必须保留同一个 PoolManager;Hook 所读取的、并决定谁可以升级它的工厂合约,固定在实现合约中。