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.

  1. 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'accepter
  2. DeployEthereumRail, avec l'adresse de la factory : l'oracle et le routeur ETH → USDC sur Ethereum, que l'owner pose sur la factory
  3. DeployBridge --sig "predict()" : affiche les adresses que prendront le hub de pont et son adaptateur
  4. DeployRemote sur 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, ou SEQUENCER_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'hui SEQUENCER_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 bouge
  5. DeployBridge : 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 place
  6. addStockMapping sur le hub de pont, pour chaque action d'un basket
  7. setBridgeHub — avant le premier marché
  8. Enregistrement des baskets : PlanBridgeBaskets affiche les appels de l'owner. Depuis le 2026-10-06, un basket compte au plus cinq actions : voir Les baskets
  9. 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 $STOCKFUN et 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 le BuybackBurner, 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 main
  10. registerStockOft sur 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 :

  • setAirdropDistributor sur la factory, fait par DeployProtocol. 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 lui
  • registerStockOft sur le contrat de l'airdrop, pour l'OFT de chaque action sur Ethereum, qui doit utiliser le même endpoint LayerZero que le contrat
  • setExclusions, 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, LaunchProtocol inscrit lui-même le déployeur, avant la frappe. Une fenêtre se mesure contre la liste en vigueur à sa fermeture : une liste de $STOCKFUN posé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 hook
  • setDeployers : les deux déployeurs, qui peuvent être remplacés
  • setVaultImplementation : l'implémentation derrière les vaults des marchés créés ensuite
  • setHoldingRecorder : 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 factory
  • setTreasuryRouter, setTreasuryOracle, setSwapRouter, setBridgeHub et setProtocolMarket

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