部署
协议跨两条链部署,部署顺序不容商量。
钱包
三个不同的角色,绝不共用同一把密钥。
| 钱包 | 能做什么 |
|---|---|
| 协议所有者 | 升级除代币、流动性锁定合约和镜像金库部署器之外的每一个模块;完成工厂合约的接入;注册篮子;设置一次性写入的地址;更改协议的各项设置;设定空投的排除名单,并登记每只股票的 OFT;在紧急情况下立即转出资产;转出模块误持的东西(rescue);启动或取消终止模式,并在其 30 天期满后回收流动性 |
| Keeper | 触发转换、跨链批次和空投:开启循环、发送股票、归入搁置的股票。自 2026-10-05 起,还支付 Hook 和锁定合约为接收方保留的款项,并收取 LP 手续费,这些调用对任何人开放 |
| 部署者 | 部署合约。在协议所有者接受所有权之前,它拥有工厂合约;在第一个跨链批次于远端枢纽上指定协议所有者之前,它管理远端枢纽 |
这些脚本从 forge 命令行或环境变量中明文的 DEPLOYER_PRIVATE_KEY 获取签名者。DeployEthereumRail、DeployProtocol、DeployRemote 和 DeployBridge 两种方式都接受;LaunchProtocol、RegisterBaskets 和 CreateMarket 只读取 DEPLOYER_PRIVATE_KEY。
自第十个审计循环起,每次广播(broadcast)都带上 --slow --skip-simulation。没有这两个参数时,forge 给每笔交易的 gas 是它自己的模拟算出的数,按以太坊 Glamsterdam 升级之前的价格计算,而升级之后创建一个合约需要四到七倍:每次创建都会耗尽 gas。带上它们,forge 会在上一笔交易被打包之后,为每笔交易采用节点的估算。因此试运行给出的 gas 数字也不能当作预算:在 Glamsterdam 下,DeployProtocol 约需 2.4 亿 gas,每个市场的启动需 1500 万到 2400 万。
顺序
顺序由三项约束决定。每个以太坊模块在构造时都要接收工厂合约的地址,因此工厂合约最先部署,随后由其所有者把其余合约接入其中。以太坊通道的路由器和预言机,与跨链桥枢纽一样,都绑定到工厂合约,因此它们在协议之后部署。两个枢纽通过预测地址相互固定。
DeployProtocol:首先是工厂合约,然后是为全部 14 个 v4 权限挖掘地址的 Hook、锁定合约、持仓记录器、各个部署器、金库实现合约、Lens、兑换路由器和空投合约AirdropDistributor,全部接入工厂合约。随后所有权转交给协议所有者,后者必须接受它DeployEthereumRail(传入工厂合约的地址):以太坊上的预言机和 ETH → USDC 路由器,由所有者将它们设置到工厂合约上DeployBridge --sig "predict()":打印跨链桥枢纽及其适配器将获得的地址DeployRemote(在 Robinhood Chain 上):首先是固定指向这些预测地址的远端枢纽,然后是股票路由器、预言机和镜像金库实现合约,由枢纽的管理员将它们接入枢纽;如果提供了空投路由和股票适配器,也一并接入(见下文)。自 2026-10-06 起,它在每次运行中都会设置远端枢纽针对以太坊上空投送达的两项 gas 策略(在空投路由之前,见下文)以及预言机的两项防护,并且在没有就排序器检查作出决定时拒绝启动:SEQUENCER_UPTIME_FEED,即 Chainlink 在 Robinhood Chain 上的 L2 排序器在线状态喂价,或者SEQUENCER_CHECK_OFF=true,即有意关闭这项检查,两者绝不能同时设置,而SEQUENCER_GRACE_PERIOD(默认 3,600 秒)只能与喂价一起设置。Chainlink 没有为 Robinhood Chain 发布这样的喂价,因此如今的主网运行会设置SEQUENCER_CHECK_OFF=true,如果之后发布了这样的喂价,由 StockFun 的所有者再设置它(setSequencerUptimeFeed)。随后,该脚本会开启每只股票的预言机暂停检查(setOraclePauseCheck),这一步在枢纽、路由器和预言机之后进行,因此任何预测地址都不会改变DeployBridge:部署在预测地址上的跨链桥枢纽及其适配器;所有者在枢纽上指定该适配器,仅此一次(setAdapter)。自 2026-10-05 起,如果工厂合约已经指定了一个枢纽,该脚本会在部署任何东西之前停止(自 2026-10-06 起,这是它的第一项检查,在预测地址之前进行);当它的签名者拥有工厂合约时,它会先映射股票,再指定枢纽setBridgeHub——在第一个市场之前。自 2026-10-05 起,它会拒绝一个无法承载已注册篮子的枢纽(UnmappedBridgeStock)- 在跨链桥枢纽上为每只篮子股票调用
addStockMapping - 注册篮子:
PlanBridgeBaskets打印所有者需要执行的调用。自 2026-10-06 起,一个篮子最多包含五只股票:参见篮子 LaunchProtocol:$STOCKFUN(其全部供应量进入它的锁定头寸)、其金库、BuybackBurner以及setBuybackWallet。它需要先指定空投合约,这一步由DeployProtocol完成:自 2026-10-05 起,它在铸造之前,就在代币将要使用的地址上把发射操作者列入$STOCKFUN的排除名单。自 2026-10-06 起,一次在协议市场被指定之前停止的运行,会沿用它留下的代币和金库继续进行(--sig "resume(address,address)"),并先对它们进行检查,而不是铸造第二个$STOCKFUN;一旦协议市场已被指定,该脚本便不再运行,其余步骤需手动完成- 由所有者在空投合约上调用
registerStockOft,为每只股票在以太坊上的 OFT 进行登记
每个可升级模块都以两个合约的形式部署:先是其实现合约,然后是其代理合约。predict() 把它们计算在内:跨链桥枢纽的代理合约位于部署者的 nonce + 1,其适配器的代理合约位于 nonce + 3。
StockFun 不配对任何 LayerZero 对端:USDG OFT 的对端属于其发行方,预检只负责检查它们。
自 2026-10-06 起,本地脚本和测试网脚本 LocalRun 与 DeployTestnetBridge 只在真正广播交易(broadcast)时才写入它们的部署文件,自第九个审计循环起,DeployTestnetRail 也是如此:试运行得到的地址上没有代码。在测试网上,DeployTestnetRail 接受同样的三个排序器输入,全部可选(没有喂价时检查保持关闭,因为 Chainlink 也没有为测试网列出这样的喂价),并开启每只股票的预言机暂停检查;以太坊脚本则让两项防护都保持关闭。测试网上的 Keeper 运行时把 KEEPER_REQUIRE_MARKET_OPEN、KEEPER_AIRDROP_AFTER_SESSION 以及自 2026-10-06 起的 KEEPER_CONVERT_ONCE_PER_WINDOW 设为 false,使它在每一轮运行中都进行转换:参见Keeper。自第七个审计循环(2026-10-06)起,Keeper 在启动之前会把每个 RPC 的链与其配置进行核对:测试网上的 Keeper 设置 KEEPER_CHAIN_ID=11155111,并在使用跨链桥时设置 KEEPER_REMOTE_CHAIN_ID=46630,而主网上的 Keeper 设置 KEEPER_CHAIN_ID=1 和 4663。自第八个审计循环起,两者都是必需的:没有 KEEPER_CHAIN_ID,或者在使用跨链桥时没有 KEEPER_REMOTE_CHAIN_ID,Keeper 都会拒绝启动。应用的数据 Worker 以同样的方式检查其端点的链,而它的公共 RPC 跟随它的两个链 id,因此测试网上的 Worker 只需要这两个值。
2026-10-06 的 LayerZero 测试网运行使用生产脚本,或保留其主体的测试网包装脚本,并配以它自己的测试代币、交易场所和适配器(位于合约中一个仅用于测试网的文件夹里),把协议部署在 Sepolia 和 Robinhood Chain 的测试网(链 46630)上。自第九个审计循环起,预检也会检查这次运行,只要提供它的两个文件,以及 SEPOLIA_RPC_URL 和 ROBINHOOD_TESTNET_RPC_URL。这次运行证明了什么、没有证明什么,见测试与验证。
空投
自 2026-10-04 起,空投合约随协议一起部署。它的设置来自环境变量:
| 脚本 | 变量 | 默认值 | 作用 |
|---|---|---|---|
DeployProtocol |
AIRDROP_LZ_ENDPOINT |
无:仅本地通道 | 以太坊上的 LayerZero 端点,用于在 Robinhood Chain 上买入的股票 |
DeployProtocol |
AIRDROP_REMOTE_EID |
有端点时为 30416 | Robinhood Chain 的 LayerZero 端点 id,送达的唯一来源 |
DeployProtocol |
AIRDROP_CYCLE_LENGTH |
86,400(24 小时) | 时间窗口的长度,以秒为单位,须为整小时 |
DeployProtocol |
AIRDROP_CYCLE_OFFSET |
46,800(13:00 UTC) | 时间窗口结束的位置,以 00:00 UTC 之后的秒数表示,须为整小时:全年都在美股开盘之前 |
DeployRemote |
AIRDROP_DISTRIBUTOR |
无 | 镜像金库发送股票的目的地,即以太坊上的空投合约 |
DeployRemote |
AIRDROP_RECEIVE_GAS、_MIN、_MAX |
650,000、200,000、1,500,000 | 自 2026-10-06 起:在 Keeper 请求默认值时,每次送达在以太坊上的 lzReceive gas(在股票 OFT 强制设定的 gas 之上),以及它可以请求的下限和上限 |
DeployRemote |
AIRDROP_COMPOSE_GAS、_MIN、_MAX |
1,250,000、600,000、4,000,000 | 每次送达在空投合约上的调用 lzCompose 的 gas,方式相同。在 2026-10-06 之前,一个数值 600,000 决定了每一次送达 |
DeployRemote |
STOCK_ADAPTERS |
无 | 逗号分隔的列表,STOCKS 的每一项对应一个 LayerZero 适配器,没有适配器的股票填零 |
这些是初始值:所有者之后可以更改时间表(setCycleSchedule)和 LayerZero 端点(setLayerZero)。在 DeployRemote 上,AIRDROP_DISTRIBUTOR 和 STOCK_ADAPTERS 是可选的:远端枢纽的管理员可以之后再设置它们(setAirdrop、setStockAdapter)。两项 gas 策略在每次运行中都会设置,取自环境变量或枢纽的默认值,并且在设置空投合约之前设置,因为空投合约的 compose gas 必须落在它们的范围之内;管理员之后可以更改它们(setAirdropReceiveGas、setAirdropComposeGas)。超过 uint128 的数值会让运行停止。接下来是所有者的步骤:
- 在工厂合约上调用
setAirdropDistributor,由DeployProtocol完成。所有者之后可以指定另一个空投合约;金库实时读取它,而被替换的空投合约会让它的每一个循环都仍可在原处领取 - 在空投合约上调用
registerStockOft,为每只股票在以太坊上的 OFT 进行登记,该 OFT 必须使用空投合约自己的 LayerZero 端点 setExclusions,仅用于需要在销毁地址之外再排除其他地址的代币:默认情况下没有任何市场代币需要;$STOCKFUN的名单(其中包括它的发射操作者)由LaunchProtocol设定
股票适配器本身,每只股票一个——Robinhood Chain 上的锁仓适配器及其在以太坊上的 OFT——其主网版本不在代码库中:它们需要 LayerZero 的 oft-evm 包。DeployRemote 接收它们的地址。2026-10-06 的 LayerZero 测试网运行使用的是测试适配器:测试股票之上的 LayerZero OFTAdapter,以及用于封装股票的 LayerZero OFT,位于它仅用于测试网的文件夹中。
来自 Robinhood Chain 的每一次送达在以太坊上分两次调用运行:股票 OFT 的 lzReceive,它把封装股票铸造给空投合约;然后是空投合约的 lzCompose,它将其计入。自 2026-10-06 起,Keeper 在每次发送时都会指定这两者的 gas,这些 gas 根据在以太坊上的模拟选定(参见Keeper),而远端枢纽会把每个数值限制在其策略之内,零表示采用默认值。这些默认值以 35 % 和 30 % 的余量覆盖了以太坊 Glamsterdam 升级之后在 Sepolia 上测得的最重情形:在那里,lzReceive 向空投合约已持有的余额送达需要 184,702 gas,一只股票的第一次送达需要 481,548 gas;compose 在循环已列出该股票时需要 105,075 gas,在 Keeper 开启的循环尚未列出该股票时需要 433,645 gas,在这次送达同时也是该循环的第一次计入时约需 531,600 gas,而在它自己开启循环时需要 962,154 gas。测试中构造的最重送达,即针对十六个历史记录很长的被排除持有者开启循环,并同时带上四只搁置的股票,按 Glamsterdam 的价格约需 280 万 gas,低于 compose 的上限。在 Glamsterdam 之前,在测试中冷启动测得:送达一个已开启的循环约需 85,000,针对一个被排除地址开启循环的送达约需 275,000,最重的情形约需 1,016,000。gas 不足的送达会失败,但不会损失任何东西:它仍存储在 LayerZero 的端点上,当只有 compose 失败时,封装股票已经在空投合约上,任何人都可以用更多 gas 再次执行它。自 2026-10-06 起,Keeper 会在它自己的上限之内亲自这样做,并对第二次失败发出警报。
工厂合约的接入
工厂合约在初始化时只带有其所有者和三个钱包;它的各项设置从默认值开始。其余一切都由所有者随后指定:
setLaunchModules:Hook 和锁定合约,仅设置一次,永久有效;两者都必须指向这个工厂合约,锁定合约还必须指向这个 HooksetDeployers:两个部署器,可以被替换setVaultImplementation:此后创建的市场的金库背后的实现合约setHoldingRecorder:新的市场代币向其报告的记录器setAirdropDistributor:金库向其移交股票的空投合约;它必须指向这个工厂合约setTreasuryRouter、setTreasuryOracle、setSwapRouter、setBridgeHub和setProtocolMarket
在锁定合约、一个金库实现合约和一个持仓记录器被指定之前,无法创建任何市场。
跨链桥枢纽与兑换路由器
setBridgeHub 只能设置一次。一旦遗漏,便无法挽回。自 2026-10-05 起,它会拒绝一个无法映射已注册篮子中每一只股票的枢纽:先在枢纽上完成映射。
setBridgeHub 必须在创建第一个市场之前调用。每个 TreasuryVault 都会在构造时固定跨链桥枢纽的地址。在该地址为零时创建的金库,将永远停留在本地通道上,永远不会向 Robinhood Chain 发送任何东西。
setSwapRouter 不是一次性写入的:所有者可以随时更改它。它不再约束任何金库:自 2026-09-28 创建者回购从代码中移除后,金库便不再读取它。它记录官方兑换路由器的地址,由预检进行检查。
Hook 及其挖掘出的地址
Hook 的地址在其低位比特中编码了它的 v4 权限:它是通过暴力搜索 CREATE2 salt 找到的。自 2026-10-02 起,挖掘的是 Hook 代理合约的地址,全部 14 个权限比特位都被置位。它取决于代理合约的字节码和其构造函数参数,而后者包含实现合约的地址。新的部署必须重新挖掘;升级则保留这个地址。
foundry.toml 必须包含 bytecode_hash = "none" 和 evm_version = "cancun",否则挖出的地址将与部署的合约不匹配。
升级
升级是协议所有者在模块的代理合约上发起的一次调用,指定新的实现合约;它立即生效。每次升级之前,contracts/script/check-storage-layouts.sh 都会将新的存储布局与记录在 contracts/storage-layouts/ 中的布局进行比较,只要变更不是追加就会失败。自 2026-10-05 起,它会比较每个结构体的每一层,包括大小,并拒绝对作为存储数组元素的结构体做任何改动:只有作为映射值的结构体,或者最后一个状态变量,才可以在其末尾增长。--write 会在有意的变更之后刷新这些记录。
远端枢纽的 gas 策略随第九个审计循环于 2026-10-06 引入。在此之前部署、随后被升级的枢纽会把两项策略都读作零,而升级到相应代码的镜像金库会拒绝每一次空投发送和报价(AirdropGasNotSet),直到两项策略都已设置。因此顺序是:升级远端枢纽,设置两项策略(setAirdropReceiveGas、setAirdropComposeGas),然后指定新的镜像金库实现合约并升级每一个镜像金库,之后才启动第九个审计循环的 Keeper,它会向枢纽询问这些策略,并以三个参数发送。在此期间,旧版 Keeper 会继续使用枢纽的默认值发送。用新代码部署的枢纽会在初始化时设置这些默认值。
USDG 适配器的批次设置随第十个审计循环于 2026-10-06 引入:跨链批次中每个市场所增加的 compose gas,以及一个批次最多携带的市场数。在此之前部署、随后被升级的适配器会把两者都读作零,并拒绝每一个批次和报价(BatchGasNotSet),直到它们被设置。因此,它的升级会在同一笔交易中设置它们(upgradeToAndCall 加上 setBatchGas(400000, 17)),然后所有者把它旧的 compose gas 1,200,000 降低到现在每个批次所需的基数(setSettings,200,000),之后才启动第十个审计循环的 Keeper,它会在每个批次时读取这一上限。只要同时就绪的市场不超过 17 个,旧版 Keeper 就能继续与升级后的适配器一起工作。测试网的适配器于 2026-10-06 以这种方式升级,它的下一个批次以新的 compose gas 顺利通过。
改变应用的数据服务所读取内容的升级,要先于该服务上线。自 2026-10-06 起,Lens 包含每个金库的状态以及 Hook 和锁定合约欠它的款项,而读取这些字段的 Worker 和应用无法读取旧版的 Lens:先升级 Lens。第七个审计循环既没有改变 Lens,也没有改变 Worker 发布内容的结构(数据模式 7):它的 Worker 和应用可以按任意顺序部署。第八个审计循环改变了结构(数据模式 8:当 Robinhood Chain 的预言机暂缓给出某只股票的价格时,每个股票价格都会说明其缺失的原因),但没有改变 Lens,它的 Worker 和应用仍然可以按任意顺序部署:较旧的应用会忽略这一原因,而这一版应用读取不带原因的旧版 Worker。第九个审计循环保持数据模式 8。
与任何 TreasuryOracle 一样,Robinhood 通道的预言机在启动时两项防护都处于关闭状态。因此,在远端枢纽上指定的替换预言机(setOracle)启动时它们同样处于关闭状态,枢纽的管理员要像部署脚本那样为它重新开启:每只股票的预言机暂停检查,以及排序器喂价(如果之前设置过)。已经初始化的镜像金库保留它们初始化时所用的预言机。
设置
设置是协议所有者在持有该设置的模块上发起的一次调用,在 Robinhood Chain 上则由远端枢纽的管理员发起;它立即生效,并发出一个事件。每个模块都从信任模型中列出的默认值开始。跨链桥适配器以部署时的 gas 开始:在 USDG 通道上是 COMPOSE_GAS,即批次最后一步中每个批次都需要的那一部分,自第十个审计循环起在 DeployBridge 中默认为 200,000(在此之前为整个步骤的 1,200,000),另加批次中每个市场 400,000,每个批次最多 17 个市场(setBatchGas;最大批次的 gas 至多为 24,000,000),另有 30 个基点的 Curve 界限;在官方跨链桥通道上则是两张票据的 gas,以及自 2026-10-05 起计算存入票据成本所依据的字节数(DeployTestnetBridge 中的 DEPOSIT_CALLDATA_LENGTH;为零时采用默认值 1,024)。远端枢纽以空投送达的两项 gas 策略开始(见上文)。
主网之前
- 一次完整的紧急模式演练:转移、暂停、解除暂停
- 在任何公开开放之前,先在一个验证用金库上进行首次小规模空投。2026-10-06 的 LayerZero 测试网运行在 LayerZero 的测试网端点、DVN 和执行器上端到端地运行了 StockFun 的代码;它既不能证明 Paxos 的 USDG 对,也不能证明 Robinhood 的股票及其适配器、真实的喂价和流动性,或者主网的 gas、费用和最终性
- 对远端链上喂价的最新调查
- 通过预检验证 USDG OFT 的 LayerZero 对端以及 USDG 的暂停状态;自第八个审计循环起,预检还会对照其清单检查预言机的防护,而该清单必须说明排序器检查的状态,如今这项检查处于关闭状态
- Paxos 的 USDG 前往 Robinhood Chain 所走路径的大小限制,从 LayerZero 的发送库读取(
getExecutorConfig),如果它不是 10,000 字节,就相应设置跨链批次的市场数上限:最多 (大小 − 392) ÷ 544 个市场(自第十个审计循环起) - 外部审查——现有的形式化证明并不涵盖跨链通道