Déploiement
Le protocole se déploie sur deux chaînes, dans un ordre qui n'est pas négociable.
Les wallets
Trois rôles distincts, jamais la même clé.
| Wallet | Ce qu'il peut |
|---|---|
| Owner du protocole | Upgrader tous les modules sauf les tokens, le lock de liquidité et le déployeur des vaults miroirs ; câbler la factory ; enregistrer les baskets ; poser les adresses write-once ; changer les réglages du protocole ; fixer les listes d'exclusion de l'airdrop et enregistrer l'OFT de chaque action ; sortir des actifs en urgence, aussitôt, et ce qui est coincé dans un module (rescue) ; lancer ou annuler le mode fin, et récupérer la liquidité une fois ses 30 jours écoulés |
| Keeper | Déclencher les conversions, les batches de pont et l'airdrop : ouvrir les cycles, envoyer les actions, placer les actions mises de côté ; payer ce que le hook et le lock doivent à un destinataire qui l'avait refusé, et encaisser les frais de LP, des appels ouverts à tous |
| Déployeur | Poser les contrats. Il possède la factory jusqu'à ce que l'owner du protocole en accepte la propriété, et administre le hub distant jusqu'à ce que le premier batch du pont y désigne l'owner du protocole |
Les scripts prennent leur signataire sur la ligne de commande forge ou dans une
DEPLOYER_PRIVATE_KEY brute de l'environnement. DeployEthereumRail, DeployProtocol,
DeployRemote et DeployBridge acceptent l'un ou l'autre ; LaunchProtocol,
RegisterBaskets et CreateMarket ne lisent que DEPLOYER_PRIVATE_KEY.
Chaque diffusion se fait avec --slow --skip-simulation, depuis la dixième boucle d'audit.
Sans eux, forge donne à chaque transaction le gas que sa propre simulation a compté, aux prix
d'avant l'upgrade Glamsterdam d'Ethereum, et une création de contrat en demande quatre à sept
fois plus après lui : chaque création tomberait à court de gas. Avec eux, forge prend
l'estimation du nœud pour chaque transaction, une fois la précédente minée. Les chiffres de gas
d'une simulation ne sont pas non plus un budget : sous Glamsterdam, DeployProtocol demande
environ 240 millions de gas, et le lancement de chaque marché 15 à 24 millions.
L'ordre
Trois contraintes le fixent. Chaque module Ethereum prend l'adresse de la factory à sa construction : la factory vient donc en premier, et son owner y câble le reste ensuite. Le routeur et l'oracle du rail Ethereum, comme le hub de pont, sont liés à la factory : ils viennent donc après le protocole. Les deux hubs s'épinglent l'un l'autre par adresse prédite.
DeployProtocol: la factory d'abord, puis le hook, miné pour les 14 permissions v4, le lock, l'enregistreur de détention, les déployeurs, l'implémentation des vaults, la lens, le routeur de swap et le contrat de l'airdrop,AirdropDistributor, tous câblés dans la factory. La propriété passe ensuite à l'owner du protocole, qui doit l'accepterDeployEthereumRail, avec l'adresse de la factory : l'oracle et le routeur ETH → USDC sur Ethereum, que l'owner pose sur la factoryDeployBridge --sig "predict()": affiche les adresses que prendront le hub de pont et son adaptateurDeployRemotesur Robinhood Chain : le hub distant d'abord, épinglé sur ces adresses prédites, puis le routeur d'actions, l'oracle et l'implémentation des vaults miroirs, que l'admin du hub y câble, avec la route de l'airdrop et les adaptateurs d'actions quand ils sont fournis (plus bas). Depuis le 2026-10-06, il pose à chaque passage les deux bornes de gas des livraisons de l'airdrop sur Ethereum, avant la route de l'airdrop (plus bas), et les deux gardes de l'oracle, et refuse de partir sans décision sur le contrôle du séquenceur :SEQUENCER_UPTIME_FEED, le feed de disponibilité du séquenceur L2 de Chainlink sur Robinhood Chain, ouSEQUENCER_CHECK_OFF=true, le contrôle éteint par choix, jamais les deux,SEQUENCER_GRACE_PERIOD(3 600 secondes par défaut) n'allant qu'avec un feed. Chainlink ne publie aucun feed de ce type pour Robinhood Chain : un déploiement mainnet passe donc aujourd'huiSEQUENCER_CHECK_OFF=true, et l'owner de StockFun posera le feed plus tard (setSequencerUptimeFeed) s'il est publié. Le script allume ensuite la pause d'oracle de chaque action (setOraclePauseCheck), après le hub, le routeur et l'oracle, si bien qu'aucune adresse prédite ne bougeDeployBridge: le hub de pont et son adaptateur, aux adresses prédites ; l'owner désigne l'adaptateur sur le hub, une fois (setAdapter). Depuis le 2026-10-05, le script s'arrête avant de déployer si la factory désigne déjà un hub (depuis le 2026-10-06, sa première vérification, avant les adresses prédites) : un hub se corrige en upgradant ses proxys en placeaddStockMappingsur le hub de pont, pour chaque action d'un basketsetBridgeHub— avant le premier marché- Enregistrement des baskets :
PlanBridgeBasketsaffiche les appels de l'owner. Depuis le 2026-10-06, un basket compte au plus cinq actions : voir Les baskets LaunchProtocol: il exige que le contrat de l'airdrop soit désigné, inscrit le déployeur sur la liste d'exclusion de$STOCKFUNà l'adresse que prendra le token, puis frappe$STOCKFUNet vérifie qu'il est à cette adresse, crée son vault, désigne le marché du protocole, dépose toute la supply dans sa position verrouillée, et déploie leBuybackBurner, qu'il désigne (setBuybackWallet). Depuis le 2026-10-06, un passage arrêté avant la désignation du marché du protocole se reprend avec le token et le vault qu'il a laissés (--sig "resume(address,address)"), vérifiés d'abord, au lieu de frapper un second$STOCKFUN; une fois le marché du protocole désigné, le script ne tourne plus, et les étapes restantes se font à la mainregisterStockOftsur le contrat de l'airdrop, par l'owner, pour l'OFT de chaque action sur Ethereum
Depuis la troisième boucle d'audit du 2026-10-05, les mappings viennent avant le hub :
setBridgeHub refuse un hub qui ne mappe pas chaque action d'un basket déjà enregistré
(UnmappedBridgeStock), et, une fois le hub désigné, l'enregistrement d'un basket refuse de
même une action que le hub ne mappe pas. DeployBridge, quand le déployeur est l'owner,
désigne l'adaptateur, pose les mappings, puis le hub ; sinon il affiche ces appels pour
l'owner.
Chaque module upgradable est déployé en deux contrats, son implémentation puis son proxy.
predict() les compte : le proxy du hub de pont vient au nonce + 1 du déployeur, celui de
son adaptateur au nonce + 3.
StockFun n'appaire aucun peer LayerZero : ceux de l'OFT USDG appartiennent à son émetteur, et le préflight se contente de les vérifier.
L'airdrop
Depuis le 2026-10-04, le contrat de l'airdrop se déploie avec le protocole. Ses réglages viennent de l'environnement :
| Script | Variable | Défaut | Rôle |
|---|---|---|---|
DeployProtocol |
AIRDROP_LZ_ENDPOINT |
Aucun : le rail local seulement | Endpoint LayerZero sur Ethereum, pour les actions achetées sur Robinhood Chain |
DeployProtocol |
AIRDROP_REMOTE_EID |
30416 avec un endpoint | Identifiant d'endpoint LayerZero de Robinhood Chain, seule source d'une livraison |
DeployProtocol |
AIRDROP_CYCLE_LENGTH |
86 400 (24 heures) | Durée d'une fenêtre, en secondes, un nombre entier d'heures |
DeployProtocol |
AIRDROP_CYCLE_OFFSET |
46 800 (13:00 UTC) | Où se ferment les fenêtres, en secondes après 00:00 UTC, un nombre entier d'heures : avant l'ouverture US toute l'année |
DeployRemote |
AIRDROP_DISTRIBUTOR |
Aucun | Le contrat de l'airdrop sur Ethereum vers lequel envoient les vaults miroirs |
DeployRemote |
AIRDROP_RECEIVE_GAS, _MIN, _MAX |
650 000, 200 000, 1 500 000 | Depuis le 2026-10-06 : le gas lzReceive de chaque livraison sur Ethereum, en plus de ce qu'impose l'OFT de l'action, quand le keeper demande la valeur par défaut, et le plancher et le plafond de ce qu'il peut demander |
DeployRemote |
AIRDROP_COMPOSE_GAS, _MIN, _MAX |
1 250 000, 600 000, 4 000 000 | Le gas de l'appel de chaque livraison sur le contrat de l'airdrop, lzCompose, de la même façon. Jusqu'au 2026-10-06, un seul chiffre, 600 000, valait pour chaque livraison |
DeployRemote |
STOCK_ADAPTERS |
Aucun | Liste séparée par des virgules, un adaptateur LayerZero par entrée de STOCKS, zéro pour une action qui n'en a pas |
Ce sont des valeurs de départ : l'owner peut changer le calendrier (setCycleSchedule) et
l'endpoint LayerZero (setLayerZero) plus tard. Sur DeployRemote, AIRDROP_DISTRIBUTOR et
STOCK_ADAPTERS sont optionnelles : l'admin du hub distant peut les poser plus tard
(setAirdrop, setStockAdapter). Les deux bornes de gas sont posées à chaque passage, depuis
l'environnement ou les valeurs par défaut du hub, avant le distributeur, dont le gas de compose
doit tomber entre elles ; l'admin peut les changer plus tard (setAirdropReceiveGas,
setAirdropComposeGas). Une valeur au-delà de uint128 arrête le script. Viennent ensuite
les étapes de l'owner :
setAirdropDistributorsur la factory, fait parDeployProtocol. L'owner peut en désigner un autre plus tard ; les vaults le lisent en direct, et un contrat remplacé garde chacun de ses cycles réclamable chez luiregisterStockOftsur le contrat de l'airdrop, pour l'OFT de chaque action sur Ethereum, qui doit utiliser le même endpoint LayerZero que le contratsetExclusions, seulement pour un token qui a besoin d'adresses exclues en plus de l'adresse de burn : aucun token de marché par défaut. Sur$STOCKFUN,LaunchProtocolinscrit lui-même le déployeur, avant la frappe. Une fenêtre se mesure contre la liste en vigueur à sa fermeture : une liste de$STOCKFUNposée avant que la première fenêtre après le lancement soit fermée doit garder le déployeur
Les adaptateurs d'actions eux-mêmes, un par action — l'adaptateur de verrouillage sur
Robinhood Chain et son OFT sur Ethereum — ne sont pas dans le dépôt pour le mainnet : ils
demandent le paquet oft-evm de LayerZero. DeployRemote prend leurs adresses. Le test
LayerZero sur testnet du 2026-10-06 a utilisé des adaptateurs de test, l'OFTAdapter de
LayerZero sur des actions de test et l'OFT de LayerZero pour les actions wrappées, dans son
dossier propre au testnet.
Le keeper mène l'étape de l'airdrop depuis le 2026-10-05, avec ses propres variables, dont
KEEPER_AIRDROP_AFTER_SESSION, à mettre à false sur un testnet, comme, depuis le
2026-10-06, KEEPER_CONVERT_ONCE_PER_WINDOW, pour qu'un keeper de testnet convertisse à
chaque passage : voir Le keeper. Depuis la septième boucle d'audit, le
2026-10-06, le keeper vérifie avant de démarrer que chaque RPC sert la chaîne de sa
configuration : un keeper de testnet fixe KEEPER_CHAIN_ID=11155111 et, avec le pont,
KEEPER_REMOTE_CHAIN_ID=46630, un keeper mainnet KEEPER_CHAIN_ID=1 avec 4663 ; depuis la
huitième boucle d'audit, les deux sont requis, et le keeper refuse de démarrer sans
KEEPER_CHAIN_ID, ou sans KEEPER_REMOTE_CHAIN_ID à côté du pont. Le Worker de
l'app vérifie de même la chaîne de ses endpoints, et ses RPC publics suivent ses deux
identifiants de chaîne : un Worker de testnet n'a besoin que d'eux. Les scripts local et de
testnet, LocalRun et DeployTestnetBridge, n'écrivent leur fichier de déploiement que
lorsqu'ils diffusent, depuis le 2026-10-06, et DeployTestnetRail aussi depuis la neuvième
boucle d'audit : les adresses d'une simulation ne portent pas de code. Sur le testnet, DeployTestnetRail prend les trois mêmes entrées du séquenceur, toutes
facultatives (sans feed, le contrôle reste éteint, Chainlink n'en listant pas non plus pour le
testnet), et allume la pause d'oracle de chaque action ; les scripts Ethereum laissent les deux
gardes éteintes.
Le test LayerZero sur testnet du 2026-10-06 a déployé le protocole sur Sepolia et sur le
testnet de Robinhood Chain (chaîne 46630) par les scripts de production, ou des enveloppes de
testnet qui gardent leur corps, avec ses propres tokens, places d'échange et adaptateurs de
test, dans un dossier des contrats propre au testnet. Depuis la neuvième boucle d'audit, le
préflight vérifie aussi ce déploiement, à partir de ses deux fichiers, avec SEPOLIA_RPC_URL et
ROBINHOOD_TESTNET_RPC_URL. Ce que le test a prouvé, et ce qu'il n'a pas prouvé, est dans
Tests et vérification.
Chaque livraison venue de Robinhood Chain s'exécute sur Ethereum en deux appels : le
lzReceive de l'OFT de l'action, qui frappe l'action wrappée au contrat de l'airdrop, puis le
lzCompose du contrat de l'airdrop, qui la crédite. Depuis le 2026-10-06, le keeper nomme le
gas des deux à chaque envoi, choisi à partir de simulations sur Ethereum (voir
Le keeper), et le hub distant tient chaque valeur dans sa borne, zéro prenant la
valeur par défaut. Les valeurs par défaut couvrent de 35 % et 30 % les cas les plus lourds
mesurés sur Sepolia après la mise à niveau Glamsterdam d'Ethereum : un lzReceive y demande
184 702 gas vers un solde que le contrat de l'airdrop détient déjà, et 481 548 pour la toute
première livraison d'une action ; un compose 105 075 quand le cycle liste déjà l'action,
433 645 quand le cycle que le keeper a ouvert ne la liste pas encore, environ 531 600 quand la
livraison est aussi le premier crédit du cycle, et 962 154 quand elle ouvre le cycle elle-même.
La livraison la plus lourde construite dans les tests, une ouverture contre seize holders exclus
aux longs historiques qui emmène aussi quatre actions mises de côté, demande environ 2,8 millions
aux prix de Glamsterdam, sous le plafond du compose. Avant Glamsterdam, mesuré à froid dans les
tests, une livraison dans un cycle ouvert prenait environ 85 000, une livraison qui ouvre un
cycle contre une adresse exclue environ 275 000, et la plus lourde environ 1 016 000. Une
livraison à court de gas échoue sans rien perdre : elle reste stockée sur l'endpoint de
LayerZero, les actions wrappées déjà sur le contrat de l'airdrop quand seul le compose a échoué,
et n'importe qui peut la relancer avec plus de gas. Depuis le 2026-10-06, le keeper le fait
lui-même, dans sa propre borne, et alerte un second échec.
Le câblage de la factory
La factory est initialisée avec son owner et ses trois wallets seulement ; ses réglages partent de leurs valeurs par défaut. L'owner désigne tout le reste ensuite :
setLaunchModules: le hook et le lock, une fois pour toutes ; les deux doivent désigner cette factory, et le lock ce hooksetDeployers: les deux déployeurs, qui peuvent être remplacéssetVaultImplementation: l'implémentation derrière les vaults des marchés créés ensuitesetHoldingRecorder: l'enregistreur auquel déclarent les nouveaux tokens de marchésetAirdropDistributor: le contrat de l'airdrop auquel les vaults remettent leurs actions ; il doit désigner cette factorysetTreasuryRouter,setTreasuryOracle,setSwapRouter,setBridgeHubetsetProtocolMarket
Aucun marché ne peut être créé tant que le lock, une implémentation de vault et un enregistreur de détention ne sont pas désignés.
Le hub de pont et le routeur de swap
setBridgeHub ne peut être posé qu'une fois. Le manquer ne se rattrape pas.
setBridgeHub doit être appelé avant la création du premier marché. Chaque
TreasuryVault épingle l'adresse du hub de pont à sa construction. Un vault créé alors que
l'adresse est nulle reste sur le rail local pour toujours et n'envoie jamais rien vers
Robinhood Chain.
setSwapRouter n'est pas write-once : l'owner peut le changer à tout moment. Il n'engage
plus aucun vault : les vaults ont cessé de le lire quand le buyback créateur a été retiré du
code, le 2026-09-28. Il enregistre l'adresse du routeur de swap officiel, que le préflight
vérifie.
Le hook et son adresse minée
L'adresse du hook encode ses permissions v4 dans ses bits de poids faible : elle est trouvée par force brute sur le sel CREATE2. Depuis le 2026-10-02, l'adresse minée est celle du proxy du hook, avec les 14 bits de permission à un. Elle dépend du bytecode du proxy et des arguments de son constructeur, qui portent l'adresse de l'implémentation. Un nouveau déploiement doit être re-miné ; un upgrade garde l'adresse.
foundry.toml doit porter bytecode_hash = "none" et evm_version = "cancun", sinon
l'adresse minée ne correspond pas au contrat déployé.
Les upgrades
Un upgrade est un appel de l'owner du protocole sur le proxy du module, qui désigne la
nouvelle implémentation ; il prend effet aussitôt. Avant chaque upgrade,
contracts/script/check-storage-layouts.sh compare la nouvelle disposition du stockage à
celle enregistrée dans contracts/storage-layouts/, et échoue sur tout changement autre
qu'un ajout ; --write rafraîchit les enregistrements après un changement voulu. Depuis le
2026-10-05, il compare chaque niveau de chaque structure, taille comprise, et tient pour
immuable la structure d'un élément de tableau en stockage : seule une structure valeur d'un
mapping, ou la dernière variable d'état, peut grandir à sa fin.
Les bornes de gas du hub distant sont venues avec la neuvième boucle d'audit, le 2026-10-06. Un
hub déployé avant elles et mis à jour lit les deux bornes à zéro, et un vault miroir mis à jour
vers le code qui va avec refuse tout envoi et tout devis de l'airdrop (AirdropGasNotSet)
jusqu'à ce que les deux soient posées. L'ordre est donc : l'upgrade du hub distant, la pose des
deux bornes (setAirdropReceiveGas, setAirdropComposeGas), puis la nouvelle implémentation des
vaults miroirs et l'upgrade de chacun, et alors seulement un keeper de la neuvième boucle, qui
demande les bornes au hub et envoie avec trois arguments. Un keeper d'avant continue entre-temps
d'envoyer avec le gas par défaut du hub. Un hub déployé avec le nouveau code pose les valeurs par
défaut à son initialisation.
Les réglages de batch de l'adaptateur USDG sont venus avec la dixième boucle d'audit, le
2026-10-06 : le gas de compose qu'ajoute chaque marché d'un batch du pont, et le plus de marchés
qu'un batch porte. Un adaptateur déployé avant eux et mis à jour les lit nuls et refuse tout
batch et tout devis (BatchGasNotSet) jusqu'à ce qu'ils soient posés. Son upgrade les pose donc
dans la même transaction (upgradeToAndCall avec setBatchGas(400000, 17)), puis l'owner
ramène son ancien gas de compose, 1 200 000, à la base que demande désormais tout batch
(setSettings, 200 000), et alors seulement démarre un keeper de la dixième boucle, qui relit le
plafond à chaque batch. Un keeper d'avant fonctionne avec l'adaptateur mis à jour tant que pas
plus de 17 marchés ne sont prêts à la fois. L'adaptateur du testnet a été mis à jour ainsi le
2026-10-06, et son batch suivant est passé à son nouveau gas de compose.
Un upgrade qui change ce que lit le service de données de l'app passe avant ce service. Depuis le 2026-10-06, la Lens porte l'état de chaque vault et ce que le hook et le lock lui doivent, et le Worker et l'app qui lisent ces champs ne savent pas lire une Lens plus ancienne : la Lens s'upgrade d'abord. La septième boucle d'audit ne change ni la Lens ni la forme de ce que publie le Worker (schéma 7) : son Worker et son app se déploient dans n'importe quel ordre. La huitième change la forme (schéma 8 : le prix de chaque action dit pourquoi il manque, quand l'oracle de Robinhood Chain le retient), pas la Lens, et son Worker et son app se déploient toujours dans n'importe quel ordre : une app plus ancienne ignore la raison, et cette app lit un Worker plus ancien sans elle. La neuvième garde le schéma 8.
L'oracle du rail Robinhood, comme tout TreasuryOracle, naît avec ses deux gardes éteintes.
Un oracle de remplacement nommé sur le hub distant (setOracle) naît donc éteint lui aussi, et
l'admin du hub les rallume pour lui, comme le fait le script de déploiement : la pause d'oracle
de chaque action, et le feed du séquenceur s'il y en avait un. Les vaults miroirs déjà
initialisés gardent l'oracle de leur initialisation.
Sans attendre un upgrade, chaque contrat qui peut détenir des fonds a, depuis le
2026-10-05, un levier de l'owner du protocole pour en sortir ce qui est coincé : le mode
urgence sur les vaults, les hubs et le contrat de l'airdrop, rescue sur les autres. Voir
Le mode urgence.
Les réglages
Un réglage est un appel de l'owner du protocole sur le module qui le porte, ou, sur
Robinhood Chain, de l'admin du hub distant ; il prend effet aussitôt et émet un événement.
Chaque module part des valeurs par défaut listées dans
Modèle de confiance. Les adaptateurs du pont partent du gas du
déploiement : sur le rail USDG COMPOSE_GAS, la part de la dernière étape d'un batch que
demande tout batch, 200 000 par défaut dans DeployBridge depuis la dixième boucle d'audit
(1 200 000 pour toute l'étape jusque-là), plus 400 000 pour chaque marché du batch et au plus 17
marchés par batch (setBatchGas ; le gas du plus gros batch au plus 24 000 000), à côté
d'une borne Curve de 30 points de base ; sur le rail canonique le gas des deux
tickets et, depuis le 2026-10-05, la longueur de calldata sur laquelle se chiffre le ticket
de dépôt, DEPOSIT_CALLDATA_LENGTH dans DeployTestnetBridge, zéro valant les 1 024 octets
par défaut, puis réglable par l'owner (setDepositCalldataLength). Le hub distant part avec
les deux bornes de gas des livraisons de l'airdrop (plus haut).
Avant le mainnet
- Répétition complète du mode urgence : transfert, pause, levée de la pause
- Un premier airdrop de petite taille sur un vault de vérification, avant toute ouverture publique. Le test LayerZero sur testnet du 2026-10-06 a mené le code de StockFun de bout en bout sur les endpoints, le DVN et l'exécuteur de LayerZero sur testnet ; il ne prouve ni la paire USDG de Paxos, ni les actions de Robinhood et leurs adaptateurs, ni de vrais feeds et une vraie liquidité, ni le gas, les frais et la finalité du mainnet
- Relevé à jour des feeds de prix sur la chaîne distante
- Vérification des peers LayerZero de l'OFT USDG et de l'état de pause d'USDG, par le préflight ; depuis la huitième boucle d'audit, le préflight vérifie aussi les gardes de l'oracle contre son manifeste, qui doit dire le réglage du contrôle du séquenceur, éteint aujourd'hui
- La limite de taille du chemin qu'emprunte l'USDG de Paxos vers Robinhood Chain, lue sur la
bibliothèque d'envoi de LayerZero (
getExecutorConfig), et le plafond de marchés d'un batch du pont ajusté si elle n'est pas de 10 000 octets : au plus (taille − 392) ÷ 544 marchés (depuis la dixième boucle d'audit) - Revue externe — les preuves formelles existantes ne couvrent pas le rail cross-chain