架构
协议运行在两条链上。以太坊承载发射台、流动性池和金库;Robinhood Chain 承载代币化股票。
合约
本表描述的是已经决定的协议。发射部分自 2026-09-28 起已写进代码,空投分发自 2026-10-04 起也已写进代码,尚未部署;各只股票的 LayerZero 适配器不在代码库中。除官方兑换路由器外,其他路由器和适配器未列入表中:以太坊上的 UniswapV4StockRouter、Robinhood Chain 上的 RobinhoodStockRouter,以及 UsdgOftAdapter。参见实施状态。自 2026-10-02 起,表中的每个合约都可升级,代币、流动性锁定合约和各个部署器除外:参见下文。图中的百分比是税的默认设置。
| 合约 | 作用 | 数量 |
|---|---|---|
StockFunFactory |
创建市场,收取创建费,维护注册表以及所有者针对新市场和金库的各项设置;以太坊上每个模块的升级授权方 | 一个 |
MarketDeployer · VaultDeployer |
通过承载代币的创建代码和金库代理合约的创建代码来绕开 EIP-170;不持有任何状态,通过工厂合约替换,而不是升级 | 各一个 |
StockFunToken |
普通的 ERC-20,不可升级,不可铸造,不可销毁;除了一个 setter setRecorder 及其救援函数之外没有自己的逻辑,二者都归协议所有者使用;向持仓记录器报告每一次余额变动 |
每个市场一个 |
HoldingRecorder |
为每个代币记录每个地址随时间变化的持仓,以及 Uniswap PoolManager 之外的供应量;如果一次转账的记录失败,该转账就会失败,这是下文隔离规则唯一的刻意例外 |
一个 |
TreasuryVault |
持有 ETH、USDC,然后是股票;负责转换(每一段买入单独进行)、在本地通道上把其股票交给空投(sendToAirdrop,每只股票单独进行),以及立即生效的紧急回收 |
每个市场一个代理合约 |
LiquidityLock |
创建池子并存入两个头寸,予以锁定,并保存每个池子的 LP 手续费和 tick 间距;为接收方保留一次手续费收取中被拒收的份额;不可升级;其唯一的出口是终止模式,于宣布 30 天之后生效 | 一个 |
StockFunHook |
对每一笔兑换收取费用,执行防狙击,保存税的设置和每个池子的白名单,拒绝锁定合约以外任何人的流动性,持有创建者、团队和回购的余额直至其被领取,并持有欠拒收其份额的金库的款项 | 一个 |
StockFunSwapRouter |
用于交易的官方路由器,白名单地址只有通过它交易才能豁免防狙击 | 一个,可由所有者替换 |
StockFunLens |
只读;为 dapp 汇总一个市场的状态,逐个单独读取每个金库;自 2026-10-06 起,它的页面还包含金库的通道、暂停状态和已通过跨链桥发送的 USDC、池子的锁定状态、代币的持仓记录器,以及 Hook 和锁定合约欠该金库的款项 | 一个 |
TreasuryOracle |
Chainlink 喂价,一次性写入;其心跳间隔是设置,自 2026-10-06 起,它在 Robinhood Chain 上的两项防护也是设置:排序器检查,在 Chainlink 为该链发布在线状态喂价之前保持关闭;以及每只股票的预言机暂停检查,它在公司行动期间暂缓给出该股票的价格 | 每条链一个 |
BuybackBurner |
买入 $STOCKFUN 并将其发送至销毁地址。只要 $STOCKFUN 池仍处于锁定状态,就不存在其他路径 |
一个 |
BridgeHub · RemoteHub · RemoteTreasuryVault |
跨链通道;远端枢纽是 Robinhood Chain 上的升级授权方,并指定空投路由和每只股票的适配器;镜像金库把其股票发送给空投(sendToAirdrop) |
一个、一个、每个市场一个代理合约 |
StockFunProtocolToken |
$STOCKFUN,与市场代币一样不可升级;其全部供应量进入它的锁定头寸 |
一个 |
AirdropDistributor |
在以太坊上,按每日循环把每个国库买入的股票分发给其代币的持有者;只为市场自己的金库记入;由每位持有者领取,一次领取会支付它能支付的每一只股票;绑定到工厂合约,由工厂合约指定 | 一个 |
代理合约与升级
自 2026-10-02 起,每个模块都是一个位于实现合约前面的 ERC-1967 代理合约,通过 UUPS 升级。代理合约持有地址和状态;实现合约持有代码,而升级会替换它。实现合约永远无法被初始化:每个代理合约都在其自身的构造函数中完成初始化。
每个模块都向一个合约询问谁可以升级它,即它的升级授权方,该授权方固定在其实现合约中:以太坊上是工厂合约,它以自己的所有者作答;Robinhood Chain 上是远端枢纽,它以自己的紧急管理员作答,即最近一个跨链批次所携带的以太坊所有者。因此,工厂合约所有权的一次转移会一次性转移以太坊上每个模块的升级权力,并随下一个批次转移 Robinhood Chain 上各模块的升级权力。如果新的实现合约指定了另一个授权方,升级就会被拒绝;Hook 和持仓记录器的新实现合约还必须保留同一个 PoolManager。
不可升级的有:代币、流动性锁定合约,以及 Robinhood Chain 上的镜像金库部署器,每一个镜像金库的地址都由它的地址推导而来。MarketDeployer 和 VaultDeployer 不持有任何状态:工厂合约会替换它们(setDeployers),而不是升级它们。
存储只追加,从不重排:每个模块的布局都记录在 contracts/storage-layouts/ 中,contracts/script/check-storage-layouts.sh 会在每次升级之前对其进行比较。自 2026-10-05 起,它会比较每个结构体的每一层,包括大小,并拒绝对作为存储数组元素的结构体做任何改动:只有作为映射值的结构体,或者最后一个状态变量,才可以在其末尾增长。
实现合约从不被直接使用,但误发到其自身地址的任何东西也有补救手段。大多数实现合约从一个 immutable 变量读取它们的授权方,因此由协议所有者把这些东西转出;工厂合约和远端枢纽的实现合约把它们的管理员保存在代理合约的存储中,因此在它们的实现合约上,掌握补救手段的是部署它们的地址。
谁可以升级,以及升级多快生效:参见信任模型。
空投合约
自 2026-10-04 起,AirdropDistributor 在以太坊上分发每个国库的股票。它和其他模块一样是一个可升级模块,即一个以工厂合约为升级授权方的代理合约。工厂合约指定它(setAirdropDistributor),金库实时读取它;被替换的空投合约会让它的每一个循环都仍可在原处领取。它从持仓记录器读取持仓,其规则见空投。
有两条路径通往它,除此之外没有任何东西能计入一个循环:
- 跨链桥通道。镜像金库的
sendToAirdrop通过远端枢纽为每只股票指定的 LayerZero 适配器(setStockAdapter),把列出的每只股票发送到远端枢纽所指定的空投合约(setAirdrop),并以该市场的 id 作为载荷。在以太坊上,该股票的 OFT 把封装股票铸造给空投合约,随后 LayerZero 的端点调用它。只有当这次送达来自所有者登记过的股票 OFT、来自 Robinhood Chain、由跨链桥枢纽为该市场推导出的镜像金库发出时,它才会将其计入。 - 本地通道。以太坊金库的
sendToAirdrop授权确切的数额,空投合约只从该市场自己的金库拉取这些数额,并按它收到的数量计入;随后授权被重新关闭。接入了跨链桥枢纽的金库会拒绝这一调用。
两条路径都由 Keeper 触发,而它只决定时机;自 2026-10-06 起,在跨链桥通道上,它还决定每次送达在以太坊上获得的 gas,远端枢纽会把这一 gas 保持在 StockFun 的所有者在那里设定的范围之内。
结构性特征
Hook 是单例。一个合约服务所有池子,从而避免为每个市场挖掘一个地址——v4 Hook 的地址在其低位比特中编码了它的权限,而找到这样一个地址需要消耗算力。自 2026-10-02 起,它是一个代理合约,其地址携带全部 14 个 v4 权限:升级会保留这个地址,而以后的实现合约可以使用任何回调。
流动性不是 NFT。它直接保存在 PoolManager 中,以锁定合约的地址为键。没有可转让的头寸,没有需要撤销的 approve,也没有会丢失的 tokenId。该合约唯一能执行的流动性操作,是一次 delta 恰好为零的 modifyLiquidity,用于收取手续费,以及通过终止模式,在宣布终止 30 天之后移除一个池子的每一个头寸。
只有锁定合约能添加流动性。自 2026-10-01 起,Hook 拒绝 StockFun 池子上的任何其他头寸,因此每一笔交易都是针对锁定头寸的兑换,并支付税。
金库没有提取功能。没有任何函数能让任何人把金库的资产发送到其自选的地址。两个出口分别是空投——它唯一的目的地是协议所指定的空投合约,后者按照一条没有人能选择的规则,按比例支付给代币的持有者——以及紧急模式,StockFun 的所有者借此把一项资产立即转移到任意地址。这些是当前实现合约的规则:StockFun 的所有者可以升级金库,并立即生效。
界限是设置,喂价不是。金库的价格界限,默认为 50 和 200 个基点,是 StockFun 所有者的设置,每个金库都实时读取;Keeper 只能收紧它们。喂价注册表对每个喂价只写入一次;喂价的心跳间隔是设置,自 2026-10-06 起,它的两项防护也是设置,这两项防护只能暂缓给出价格,绝不会改变价格:排序器检查(在 Chainlink 为 Robinhood Chain 发布在线状态喂价之前,在该链上保持关闭)和每只股票的预言机暂停检查(对 Robinhood Chain 上的每只股票开启;参见Robinhood 通道)。紧急模式两者都不触及:它转移资产,但不改变执行规则。与金库一样,注册表也可以由 StockFun 的所有者升级。
隔离与补救手段
2026-10-05,创始人确立了一条设计规则:当某件事让一个功能失败时,其他功能不得为此付出代价;一切都必须能够继续运作,一切都必须有补救手段,用以找回损失的资金并接受修复。当天的第四个审计循环把它写进了代码,从那以后,审计循环把对这条规则两部分中任何一部分的违反都算作缺陷。
隔离。一个功能、市场、股票、循环、记录或接收方的失败,绝不会阻断其他部分:该项会被跳过、作为欠款保留或被延后,并发出一个事件,其余部分照常进行。
- 跨链批次会排除金库无法释放其现金的市场(
MarketSkipped),其他市场照常跨链。现金代币拒绝转给某个镜像金库的送达会留在远端枢纽上,作为欠该市场的款项(DeliveryRefused),其他市场照常获得支付 - 一次买入单独执行每只股票的那一段(
LegFailed),一次向空投的发送也单独发送每只股票(AirdropSendFailed) - 一次领取支付它能支付的每一只股票,并延后其他股票(
ClaimDeferred) - 拒收 ETH 的金库不再让其市场的交易停止:Hook 把它未能支付的部分作为欠该金库的款项保留(
treasuryOwed),锁定合约则为接收方保留一次手续费收取中被拒收的份额(vaultOwed、creatorOwed)。一旦接收方重新接收 ETH,任何人都可以支付这些款项(payTreasury、payOwed),Keeper 每个循环都会这样做 - Lens 在单独的调用中读取每个金库,因此一个无法响应的金库不会妨碍读取其他金库(
vaultReadable)。自 2026-10-06 起,这一调用还包含金库的通道、暂停状态和已通过跨链桥发送的 USDC,页面则包含 Hook 和锁定合约欠该金库的款项:应用的数据服务对每个市场不再读取任何其他内容,因此一个耗尽自身 gas 的金库只会让它自己的数值读取失败。自第七个审计循环起,它还把每一个可升级模块(Hook、预言机、空投合约、两个枢纽)和协议代币各自放在单独的一组中读取,因此一个在有缺陷的升级之后耗尽自身 gas 的模块,只会让它自己的数值读取失败
补救手段。每一个可能持有 ETH 或代币的合约,即使只是途经或出于失误,都有一个补救手段,用来转出被卡住的东西;每一个模块都能接受修复:一次升级,或者一个替换该模块的 setter。
- 在交易之间不为任何人保管任何东西的模块——工厂合约、Lens、预言机、持仓记录器、股票路由器、官方兑换路由器和跨链桥适配器——都有协议所有者的
rescue(asset, amount, to) - 保有自己账目的模块——金库、两个枢纽和空投合约——有紧急模式
- Hook 的
rescue只取走误发给它的东西,绝不取走它所欠的款项。锁定合约的救援绝不取走它所保留的份额,也不取走头寸,只有终止模式才能触及头寸。BuybackBurner的救援只有在任何销毁都无法再花费其 ETH 时才会取走这些 ETH。不可升级的代币可以转出误发到它们自身地址的东西 - 按设计没有补救手段的:以太坊上的两个部署器和镜像金库部署器,它们不持有任何状态,不接收 ETH,也没有所有者
详见紧急模式。
唯一的刻意例外。代币向其持仓记录器的报告仍然是阻断性的:如果报告失败,转账就会失败。如果允许报告失败,持有者就可以在自己的转账中让它耗尽 gas,使记录漏掉这次变动,从而让自己的空投份额增大。补救手段立即生效,各只需一笔交易:在代币上调用 setRecorder(0),停止它的记录,或者就地升级记录器。参见空投。
链下软件包
| 软件包 | 作用 |
|---|---|
shared/ |
生成的 ABI、协议常量、格式化工具。供其他所有部分共享的单一来源 |
backend/ |
价格服务。隔离 dapp 唯一的外部依赖 |
keeper/ |
转换、跨链批次、远端路由、送达监控,以及自 2026-10-05 起的空投步骤、支付 Hook 和锁定合约为接收方保留的款项,以及收取 LP 手续费 |
cairn-app/ |
应用,一个 Next.js 前端,自 2026-10-02 起位于 projet/ 旁边,当时它取代了 React + Vite 的 dapp:参见dapp |
cairn-worker/ |
应用的数据边缘层:为所有打开的页面只读取一次协议,并推送变化 |