Le keeper

Un worker offchain qui déclenche la conversion de chaque trésorerie, puis son airdrop. Il ne décide de rien. Il fait un passage toutes les cinq minutes par défaut. Pendant la séance US, il convertit l'ETH d'un vault une fois par fenêtre de l'airdrop, toutes les 24 h par défaut, au premier passage après la fermeture de la fenêtre et seulement si le vault détient alors le seuil (depuis le 2026-10-06 : « La cadence des conversions », plus bas) ; il envoie les actions de chaque marché au contrat de l'airdrop une fois par fenêtre, après la clôture de la séance par défaut. Son étape de l'airdrop est écrite depuis le 2026-10-05 : voir plus bas.

Ce qu'il fait

À chaque passage, quelle que soit la séance :

  • Signale aussitôt, par son webhook d'alerte, chaque transfert d'urgence qu'il voit (EmergencyExecuted) sur un vault, le hub de pont, le contrat de l'airdrop, le hub distant ou un vault miroir. Depuis la septième boucle d'audit, le 2026-10-06, il n'attend le webhook que dix secondes et lit sa réponse : une alerte que le webhook ne prend pas est gardée, cent au plus, dans son fichier d'état, et renvoyée aux passages suivants, si bien qu'elle arrive en retard plutôt que jamais ; le message porte les champs que Slack et Discord lisent chacun. Depuis la huitième boucle d'audit, il garde une seule entrée par alerte distincte : une alerte levée de nouveau pendant qu'elle attend (un vault qui échoue à chaque passage) est comptée, pas gardée deux fois. Le message dit quand l'alerte a été levée la première et la dernière fois, et combien de fois, et une alerte livrée en retard commence par le dire. Depuis la neuvième boucle d'audit, les alertes gardées partent dans l'ordre de leur dernière levée : une alerte levée de nouveau passe derrière les autres, si bien que ce qui est dit en dernier d'un sujet est son état le plus récent (une surveillance qui échoue, se rétablit et échoue de nouveau pendant que le webhook est en panne est livrée « en échec » en dernier, là où elle finissait sur « rétablie »). Au-delà de cent, seule une alerte levée à chaque passage tant que dure sa cause (un vault qui échoue à convertir, le batch de pont qui échoue) part la première, si bien qu'une alerte unique, un transfert d'urgence par exemple, et le dernier mot de chaque épisode ne sont jamais évincés. Depuis la dixième boucle d'audit, le 2026-10-06, il nomme les contrats qu'il surveille par groupes, une requête de journaux par groupe, chacune nommant au plus KEEPER_LOG_MAX_SELECTORS adresses et valeurs d'événement (1 000 par défaut), et ne passe une tranche de blocs qu'une fois chacun de ses groupes lu. Jusque-là, une seule requête les nommait tous : une fois le registre des marchés plus grand que ce qu'accepte un endpoint (2 000 adresses et valeurs chez les nœuds de Robinhood Chain, neuf adresses chez l'endpoint public de Sepolia), chaque requête était refusée et plus aucun transfert d'urgence n'était relayé. Depuis la même boucle, le texte d'une erreur, dans son journal, ses alertes et son fichier d'état, ne garde d'une adresse de RPC que son schéma et son hôte, jamais le reste, où les fournisseurs placent la clé
  • Depuis la dixième boucle d'audit, lit ce que chaque passage demande de chaque marché (l'ETH, l'USDC et la fenêtre d'un vault, les dettes, le dernier airdrop) par Multicall3, cent marchés par appel, au bloc même où chaque lecture se faisait seule ; une lecture qui ne revient pas se fait seule, comme avant, jamais prise pour un zéro. Jusque-là, chaque marché jamais créé coûtait ses lectures une à une à chaque passage, actif ou non, et 2 000 marchés inactifs faisaient durer un passage autant que l'intervalle entre deux
  • Mène l'étape de l'airdrop et suit ses livraisons (plus bas)
  • Paie, depuis le 2026-10-05, ce que le hook et le lock de liquidité gardent pour un destinataire qui l'a refusé : ce que le hook doit au vault d'un marché (payTreasury), et les parts d'un encaissement que le lock garde pour le vault ou pour le créateur (payOwed), pour le pool de chaque marché et celui de $STOCKFUN. Chaque paiement est simulé d'abord, et payOwed n'est envoyé que s'il paie quelque chose. Tant que le destinataire refuse, une alerte par épisode dit qu'il attend l'admin, un upgrade du vault ou du hook ; l'épisode se clôt une fois la dette payée, par lui ou par n'importe qui
  • Encaisse, une fois par jour au plus, les frais de LP d'un pool créé avec une commission de LP, aucune par défaut : collectFees sur le lock, que n'importe qui peut appeler, simulé d'abord et envoyé seulement s'il rapporte au moins le minimum fixé. Une part que son destinataire refuse est gardée par le lock et payée comme plus haut
  • Depuis la sixième boucle d'audit, le 2026-10-06, sur le rail canonique : suit les deux tickets de chaque batch de pont jusqu'à savoir chacun exécuté, quel que soit le crédit de son transfert, et rejoue lui-même un ticket encore vivant, la nuit et le week-end compris. Un ticket qu'il ne sait pas exécuté six heures après le départ de son batch, par défaut (KEEPER_TICKET_ALERT_HOURS), lève une alerte, une seule ; un ticket disparu à son échéance ou après sans exécution vue, ou dont la création a échoué, en lève une aussi et sort du suivi. Jusque-là, le keeper cessait de suivre les tickets d'un batch dès que ses marchés étaient crédités, ce qui pouvait laisser expirer un dépôt qui avait manqué son exécution automatique. Depuis la septième boucle d'audit, il lit chaque ticket dans l'ordre du SDK Arbitrum : le reçu de sa création d'abord, puis son exécution automatique, puis son existence à un bloc au moins égal à celui de sa création ; depuis la huitième boucle d'audit, quelques blocs sous le dernier (KEEPER_REMOTE_LOG_LAG_BLOCKS), ou le bloc de sa création s'il est plus récent, un bloc que tous les nœuds d'un endpoint ont. Un dépôt créé entre deux de ses lectures, ou vu par deux nœuds à un bloc d'écart, n'est plus jamais pris pour exécuté tant qu'il est vivant
  • Depuis la septième boucle d'audit, vérifie avant son premier passage que chaque RPC sert la chaîne que sa configuration nomme, et refuse de démarrer sinon. Découverte plus tard, une mauvaise chaîne arrête le travail sur cette chaîne, avec une alerte ; une chaîne dont l'identifiant ne se lit pas attend, et le travail sur l'autre continue. Depuis la huitième boucle d'audit, les deux identifiants de chaîne doivent être fixés (plus bas), et un fichier d'état écrit pour une autre chaîne reste intact tant que le keeper n'a pas lu la chaîne que sert son RPC (« Son état »)
  • Depuis la neuvième boucle d'audit, pendant une minute après le minage de l'une de ses propres transactions, lit ce qu'elle a changé à son bloc, jamais au dernier, et y estime le gas de ce qu'il envoie ensuite. Le test sur testnet a montré pourquoi : un batch de pont envoyé juste après la conversion de la même passe était simulé sur un nœud d'un endpoint à répartition de charge en retard d'un bloc sur la conversion, ne voyait rien à passer et levait une fausse alerte. Un nœud en retard sur ce bloc répond désormais une erreur, et le batch attend une passe, avec un avertissement. Chaque transaction du keeper part aussi avec son estimation de gas fois 1,25, depuis que la mise à niveau Glamsterdam d'Ethereum a alourdi les appels de ses transactions
  • Depuis la neuvième boucle d'audit, une heure lue sur une chaîne qu'aucune date ne peut tenir (le début d'un feed, la fin d'une fenêtre, l'échéance d'un ticket) se lit « an unknown time » au lieu de faire échouer la lecture qui l'a rencontrée

Pendant la séance US seulement :

  • Repère, au premier passage après la fermeture de la fenêtre (13:00 UTC par défaut, avant l'ouverture US toute l'année), les vaults qui détiennent au moins le seuil de conversion, 0,1 ETH par défaut, lu sur la factory ; un vault en dessous attend alors la fenêtre suivante, même si des trades lui font franchir le seuil plus tard dans la journée. Fait avancer l'USDC qu'un vault détient déjà, seuil atteint ou non : depuis la sixième boucle d'audit, une fois après chacune de ses conversions d'ETH, et au plus une fois par fenêtre sinon (« La cadence des conversions », plus bas)
  • Propose des routes de swap et calcule des montants minimaux à partir d'un devis frais ; depuis le 2026-10-05, les vaults tiennent ces minimums sur ce qui arrive réellement. Depuis le 2026-10-06, chaque vault d'Ethereum se cote sur son propre routeur, celui de sa création
  • Dimensionne chaque étape, pour que la place d'échange l'exécute dans la borne du vault (plus bas)
  • Appelle la conversion, le batch de pont, puis l'achat sur la chaîne distante ; depuis le 2026-10-05, il est le seul à pouvoir envoyer le batch de pont (bridgeReady). Sur le rail canonique, il demande les frais du batch à deux fois le dernier base fee (quoteBridgeAt), et l'excédent lui revient. Depuis la neuvième boucle d'audit, il ne se rabat sur le devis d'un hub plus ancien que lorsque le hub lui-même répond qu'il n'en a pas, jamais quand un nœud se tait : un silence retient le batch une passe, avec une alerte sur le rail canonique, dont les frais suivent le base fee, et un simple avertissement sur le rail USDG. Depuis la dixième boucle d'audit, un batch du rail USDG porte au plus le plafond de marchés de l'adaptateur, 17 par défaut, qu'il relit à chaque batch : les marchés qui attendent depuis le plus longtemps passent d'abord, et les autres gardent leur USDC dans leur vault jusqu'à son passage suivant, si bien qu'aucun n'attend pour de bon (voir Le rail Robinhood). Jusque-là, tous les marchés prêts partaient dans le même batch, dont un gros batch pouvait épuiser le gas fixe du compose
  • Pré-déploie le vault miroir de chaque nouveau marché sur Robinhood Chain avant son premier batch (predeploy), ce que seuls lui et l'owner de StockFun peuvent faire depuis le 2026-10-05. Le hub distant apprend le keeper d'un batch : avant le premier, c'est l'owner de StockFun qui pré-déploie le vault du premier marché. Le keeper ne pré-déploie que si sa clé Robinhood Chain est celle que le hub distant connaît comme keeper, ou celle de son admin. Un marché dont le keeper ne peut pas pré-déployer le vault (avant le premier batch, après un changement de keeper que le hub distant n'a pas encore appris, ou quand le pré-déploiement échoue) est laissé hors du batch, avec une alerte, et les autres marchés passent. Un marché dont il ne peut pas lire le vault miroir à ce passage attend le suivant
  • Surveille les livraisons : un transfert qui n'arrive pas, un vault miroir pas encore déployé (les tickets du rail canonique, eux, sont suivis et rejoués à chaque passage, quelle que soit la séance : plus haut). Un transfert qu'il n'a pas pu mesurer sur Robinhood Chain à son départ n'est confirmé que par les propres événements du hub distant, jamais par un solde lu plus tard ; sur le rail canonique, depuis le 2026-10-06, c'est le cas de tout transfert, le hub distant payant ses enregistrements sur un cash commun, qu'un autre batch peut grossir. Il lit ces événements quelques tranches à la fois, en reprenant là où la recherche de chaque transfert s'est arrêtée ; depuis la septième boucle d'audit, seulement jusqu'à quelques blocs sous le dernier bloc de la chaîne, si bien qu'un événement des tout derniers blocs se lit à un passage suivant au lieu d'être manqué derrière un nœud en retard d'un bloc ou deux. Depuis la huitième boucle d'audit, il ne suit un batch de pont qu'une fois le bloc qui le tient enfoncé d'autant de blocs (KEEPER_LOG_LAG_BLOCKS), sur son reçu relu alors : les identifiants d'un batch sur Robinhood Chain viennent de ce bloc, et une réorganisation des derniers blocs d'Ethereum qui déplaçait le batch laissait le keeper sur des identifiants qui n'existent pas. Depuis la dixième boucle d'audit, sur le rail USDG, son alerte pour un transfert pas crédité à temps dit où en est le message du batch sur l'endpoint de Robinhood Chain : pas encore livré ; composé, le crédit suivant ; ou stocké, son USDG sur le hub distant, enregistré nulle part tant que quelqu'un ne relance pas à la main la dernière étape avec plus de gas, l'alerte donnant le hash du message stocké. Jusque-là, elle ne donnait que ce qu'enregistre le hub distant, dont le zéro se lisait « rien n'est arrivé » quand une dernière étape à court de gas avait laissé le cash sur le hub
  • Depuis la huitième boucle d'audit, lit les deux gardes de prix de l'oracle des vaults miroirs sur Robinhood Chain : tant que l'oracle retient tous les prix pour le séquenceur, les achats distants attendent, avec une alerte, et une action en pleine opération sur titres attend seule (« Quand l'oracle retient les prix », plus bas)
  • Alerte après des échecs répétés sur un vault, trois de suite par défaut (KEEPER_ALERT_AFTER_FAILURES, le même seuil que pour les jambes, les vaults illisibles, les pools et les marchés de l'airdrop, plus bas), en comptant séparément son étape sur Ethereum et son étape sur Robinhood Chain, si bien qu'un vault qui échoue à chaque passage d'un seul côté est signalé
  • Paie le cash en attente sur le hub distant par un sweep, sur les deux rails : les enregistrements dont les tokens sont arrivés, ce qui a atteint le hub pendant sa pause, et la part qu'un vault miroir avait refusée. Sur le rail USDG, il s'abstient tant que le hub distant attend que l'owner de StockFun solde un transfert d'urgence, le signale une fois, et reprend une fois les comptes soldés. Sur le rail canonique, il ne lance le sweep d'une part refusée que si ce sweep peut payer : le vault prend de nouveau le cash, ou la file a du cash au-delà de ce que retiennent les parts refusées
  • Déclenche le BuybackBurner, en comptant le solde buyback crédité sur le hook, que le burner prélève avant d'acheter

La cadence des conversions

Le fondateur a décidé le 2026-09-27 que l'ETH d'un vault se convertit une fois par cycle quotidien, juste avant l'airdrop, et seulement si le vault détient le seuil à ce moment-là. Le keeper s'y tient depuis le 2026-10-06 ; jusque-là, il convertissait l'ETH d'un vault à chaque passage de la séance dès que le vault détenait le seuil.

  • La fenêtre. Le cycle est la fenêtre de l'airdrop : elle se ferme quand le contrat de l'airdrop le dit (24 h fermées à 13:00 UTC par défaut), marché $STOCKFUN compris ; tant que la factory ne nomme pas de contrat de l'airdrop, selon ce calendrier par défaut
  • Une vérification par fenêtre. La boucle passe toujours toutes les cinq minutes par défaut, chaque passage se terminant par un rapport. Le seuil se vérifie au premier passage qui atteint le vault après la fermeture de la fenêtre. Au moins le seuil, l'ETH se convertit, une fois : le relevé que le vault tient de sa dernière conversion d'ETH, lu sur la chaîne, le dit aux passages suivants, si bien qu'un redémarrage ne change rien. En dessous, l'ETH attend la fenêtre suivante, même si des trades lui font franchir le seuil plus tard dans la journée ; le keeper garde ce constat dans son fichier d'état. Depuis le 2026-10-06, il le garde sous la forme de la fin de la fenêtre pour laquelle il l'a fait, jamais de l'heure de sa machine, qu'une horloge un peu décalée autour de la fermeture pouvait écarter de la fenêtre de la chaîne ; un constat écrit par un keeper plus ancien ne compte pas, si bien que, dans la fenêtre de la mise à jour, un tel vault est vérifié de nouveau. Depuis la sixième boucle d'audit, le constat se fait sur un solde lu après la fermeture de la fenêtre : un passage à cheval sur la fermeture inscrivait le solde lu avant elle, et un vault qui avait franchi le seuil à temps sautait cette fenêtre
  • Une conversion qui ne peut pas partir à ce moment (un feed ETH/USD muet, une place qui ne remplit pas dans la borne du vault, une transaction qui échoue, une fenêtre que le keeper ne sait pas lire) est retentée aux passages suivants de la même fenêtre. Une fenêtre qu'il ne sait pas lire compte comme un échec de l'étape ETH, alerté après des échecs répétés, et l'USDC du vault avance quand même
  • L'USDC, depuis la sixième boucle d'audit. L'USDC déjà dans un vault avance seuil atteint ou non, à son propre rythme : son achat d'actions sur Ethereum, ou sa libération dans un batch de pont, part une fois après chacune des conversions d'ETH du vault, et au plus une fois par fenêtre sinon (un don, un remboursement, ce qu'une étape ratée ou divisée a laissé). Seule compte une étape qui est passée ; une étape qui échoue est retentée aux passages suivants. La décision du fondateur du 2026-09-27 s'étend ainsi à l'USDC : jusque-là, quelques unités d'USDC envoyées à un vault avant chaque passage faisaient envoyer au keeper un achat, ou tout un batch de pont avec ses frais, à chaque passage
  • Ce qui part à chaque passage : les achats sur Robinhood Chain. Le solde d'un vault miroir ne distingue pas une livraison d'un don, et les plafonds de chaque achat étalent exprès une grosse livraison sur plusieurs passages. L'ETH qu'une conversion divisée par deux laisse (la place n'a pas pu remplir tout le solde dans la borne) attend la fenêtre suivante
  • Les runs locaux et de testnet mettent KEEPER_CONVERT_ONCE_PER_WINDOW à false, et convertissent, achètent et passent le pont alors à chaque passage ; partout ailleurs, la variable reste à sa valeur par défaut, true

Une panne à la fois

Depuis le 2026-10-05, le keeper suit la règle du protocole : une panne ne bloque jamais le reste. Il lit ce que chaque contrat dit de chaque élément, et n'alerte que sur ce qui reste bloqué.

  • Une jambe. Une jambe d'achat qu'un vault déclare échouée (LegFailed) garde son cash réservé et n'est pas comptée comme convertie ; elle alerte après trois échecs de suite pour ce vault et cette action. Chaque jambe est planifiée à part : une jambe qu'il ne peut pas planifier est sautée seule
  • Un vault. Un vault dont les lectures échouent, après un upgrade raté par exemple, est laissé hors du passage, et alerte après trois passages de suite ; les autres convertissent
  • Un marché du batch de pont. Un marché que le hub de pont laisse hors du batch (MarketSkipped) attend le batch suivant ; laissé dehors trois batches de suite (KEEPER_BRIDGE_ALERT_AFTER_SKIPS), il alerte une fois, avec la raison décodée et sa correction. Le keeper ne suit que les marchés que le batch a réellement emportés. Depuis la dixième boucle d'audit, un marché que le plafond de l'adaptateur laisse hors d'un batch attend le passage suivant, noté au journal, et ouvre ce batch-là ; un batch que l'adaptateur refuse (au-delà de son plafond, ou un adaptateur mis à jour sans ses réglages de batch) est alerté avec la raison nommée et les marchés du batch lui-même
  • Une livraison refusée. Une livraison que le token de la jambe cash refuse à un vault miroir (DeliveryRefused) est signalée une fois, avec sa correction, tant que le hub distant doit quelque chose de cette part à ce marché
  • Un pool. Un pool dont les dettes ne se lisent pas ou ne se paient pas alerte après trois passages de suite, et le suivant est traité

Chaque passage se termine par un rapport. Depuis le 2026-10-05, il compte aussi les dettes payées (debtsPaid), les pools qui doivent encore (debtsOwed), les jambes échouées (legsFailed), les encaissements de frais de LP (feesCollected) et, pour l'airdrop, les cycles ouverts, les placements, les envois et les livraisons en route. Depuis le 2026-10-06, un envoi dans une fenêtre sans détention éligible, que le contrat de l'airdrop met de côté, est compté à part (airdropHeldAside), plus avec les envois ; sur le rail canonique, le rapport compte les tickets rejoués (redeemed) et ceux encore suivis (ticketsWatched). Depuis la septième boucle d'audit, il compte aussi les marchés dont l'envoi à l'airdrop attend de valoir son coût (airdropBelowCost) et les alertes gardées pour le webhook (alertsUndelivered), et dit quand l'identifiant d'une chaîne n'est pas encore vérifié (deferred: chain-unverified).

Quand l'oracle retient les prix

Depuis le 2026-10-06, l'oracle des vaults miroirs peut retenir un prix (voir Le rail Robinhood), et depuis la huitième boucle d'audit le keeper le lit avant de coter quoi que ce soit :

  • Le séquenceur. Une fois par passage, il demande à l'oracle ce que dit son contrôle du séquenceur. Tant que le séquenceur est arrêté, revenu depuis au plus le délai de grâce, ou que son feed de disponibilité ne se lit pas, aucune action ne peut être évaluée : l'achat du vault attend, rien n'est coté et rien ne compte comme un échec. Une alerte ouvre l'épisode, gardée dans le fichier d'état pour qu'un redémarrage ne la relève pas, et une autre dit quand les achats reprennent. Le contrôle reste éteint sur Robinhood Chain tant que Chainlink n'y publie pas de feed de disponibilité : aujourd'hui, rien de cela ne peut arriver
  • Une opération sur titres. Une action dont le token met son oracle en pause est laissée hors de l'achat d'emblée, notée au journal, son USDG gardé pour elle ; elle ne compte jamais pour l'alerte d'échecs, et les autres actions du basket sont achetées. Une jambe que le vault déclare échouée pour cette raison, entre le plan du keeper et son achat, est attendue et ne compte pas non plus
  • La raison, dite. Quand la borne d'une jambe ne se lit pas parce qu'une garde retient son prix, le keeper note la raison, sur l'une ou l'autre chaîne, plutôt qu'un vault qui ne peut pas coter la jambe. Une garde qu'il ne peut pas lire (un oracle d'avant les gardes) ne retient rien : la borne de chaque jambe décide, comme avant. Depuis la neuvième boucle d'audit, les autres erreurs de l'oracle se décodent aussi : un feed périmé se lit StalePrice dans le journal et dans la raison d'une jambe échouée, là où se lisait un code non décodé

L'étape de l'airdrop

Le côté contrat est codé depuis le 2026-10-04, l'étape du keeper depuis le 2026-10-05. Le keeper traite chaque marché du registre et le marché $STOCKFUN, une fois par fenêtre, selon ce que dit la chaîne : un vault est servi pour la fenêtre quand son dernier envoi (lastAirdropAt) est à ou après la fin de la fenêtre en cours du marché (currentCycleEnd), si bien qu'un redémarrage ne change rien. Par défaut, il n'envoie qu'une fois la séance US du jour terminée, ce qui met les actions achetées dans la journée dans la fenêtre fermée ce jour-là, et, depuis la septième boucle d'audit, seulement ce qui vaut ce que son envoi coûte (plus bas) ; jusque-là, dès que le vault détenait des actions. Depuis la dixième boucle d'audit, avec ce réglage par défaut, un vault lu après la clôture sans rien à envoyer n'est relu qu'à l'ouverture de la séance suivante, ou quand la fenêtre change, sur les rails dont les achats suivent la séance de New York (pas celui d'Ondo) : ce qu'il détient vient de ses achats, faits en séance. Pas tant qu'un achat à lui attend encore son reçu, qui peut arriver dans la soirée : ce vault est relu à chaque passage, et ce que l'achat apporte part dans la fenêtre fermée ce jour-là. Pour chaque marché :

  1. Placer les actions que le marché a mises de côté, s'il y en a, même quand le vault n'a rien à envoyer : assignUnassigned, cinq appels au plus par passage, jusqu'à ce qu'elles soient placées, ce qui ouvre le cycle de la fenêtre qui les reçoit, ou qu'il ne reste aucune fenêtre finie à examiner. Depuis le 2026-10-06, le keeper fait cette recherche que le vault ait ou non quelque chose à envoyer, et quelle que soit la pause du vault : jusque-là, il ne la faisait qu'avant un envoi, si bien qu'un marché qui avait tradé une fois puis plus rien gardait son premier airdrop, mis de côté, hors de portée de ses holders, jusqu'à un nouveau trade ou une recherche lancée à la main. Une recherche pas finie retient au passage suivant un envoi dans une fenêtre sans détention éligible. Depuis la sixième boucle d'audit, tant que le vault n'a rien qui vaille d'être envoyé, une recherche qui ne placerait rien (aucune fenêtre ne peut encore prendre les actions) ne part que lorsque la recherche du marché a maxWindowsPerAssign fenêtres de retard, 30 par défaut : une recherche par mois au lieu d'une par jour, pour toujours. Une recherche qui place les actions part toujours. Depuis la septième boucle d'audit, « rien qui vaille d'être envoyé » suit la règle de l'envoi : sur le rail du pont, la poussière que l'OFT ne transporte pas, et que chaque envoi laisse au vault miroir, ne compte pas, si bien que la limite y vaut aussi
  2. Ouvrir le cycle de la fenêtre, openCycle, seulement devant un envoi, et, depuis la septième boucle d'audit, devant un envoi qui vaut son coût, ouverture comprise : jamais pour une fenêtre où rien n'est envoyé. Une fenêtre sans détention éligible n'est pas une erreur : les actions sont alors mises de côté
  3. Envoyer les actions : sendToAirdrop sur le vault Ethereum, sur le rail local ; sur le vault miroir, sur le rail du pont, en payant quoteSendToAirdrop plus une marge, 10 % par défaut, l'excédent lui étant rendu. Avant un envoi depuis Robinhood Chain, il vérifie que le hub distant envoie au contrat de l'airdrop que désigne la factory, que chaque action a un adaptateur, que cet adaptateur répond, et que son OFT est enregistré sur le contrat de l'airdrop. Depuis la septième boucle d'audit, l'envoi ne porte que les actions qui valent leur coût, ses frais cotés de nouveau pour elles. Depuis la neuvième, l'envoi et chacun de ses devis portent le gas que ses livraisons reçoivent sur Ethereum, que choisit le keeper (plus bas)
  4. Suivre chaque livraison venue de Robinhood Chain jusqu'à son exécution sur Ethereum. Depuis la neuvième boucle d'audit, une livraison bloquée sur l'endpoint d'Ethereum est réexécutée par le keeper lui-même (plus bas). Une livraison pas exécutée au bout de 60 minutes par défaut lève une alerte qui porte les commandes qui lancent ses étapes à la main, que n'importe qui peut lancer

Placer d'abord les actions mises de côté compte : un envoi qui ne trouve aucune détention éligible alors que des fenêtres antérieures restent à examiner échoue, tant qu'assignUnassigned n'a pas tourné.

Chaque action à part. Une action dont le solde ne se lit pas (gelée par son émetteur, BalanceUnreadable), sans adaptateur, dont l'adaptateur ne répond pas (peers() : pas de contrat à son adresse, ou pas une app LayerZero ; depuis le 2026-10-06, jusque-là elle faisait échouer tout l'envoi du marché), dont l'OFT n'est pas enregistré, dont l'adaptateur ne peut pas coter les frais LayerZero (depuis la huitième boucle d'audit, plus bas), ou que le reçu de l'envoi marque AirdropSendFailed, est laissée au vault avec une alerte, et les autres partent ; elle part avec l'envoi suivant une fois réparée. Chaque marché à part. Un marché dont la lecture, le placement, l'ouverture ou l'envoi échoue alerte après trois échecs de suite, et le suivant est traité. Depuis le 2026-10-06, une recherche des actions mises de côté qui échoue sans cesse a sa propre alerte, qui donne sa correction : n'importe qui peut appeler assignUnassigned pour ce marché.

En attente de l'admin. Ces états ne sont jamais comptés comme des échecs ; chacun lève une alerte par épisode, et sa fin est notée :

  • le contrat de l'airdrop est en pause : rien ne part jusqu'à unpause ;
  • il est à court d'une action après un transfert d'urgence (shortfall) : cette action attend un restore ou une passation en perte, et les autres partent ;
  • le hub distant envoie à un autre contrat de l'airdrop que celui de la factory : rien ne part de Robinhood Chain jusqu'à ce que son admin le corrige (setAirdrop) ;
  • depuis la neuvième boucle d'audit, le hub distant n'a pas ses bornes de gas des livraisons (AirdropGasNotSet, un hub mis à jour depuis une version qui ne les avait pas) : rien ne part de Robinhood Chain jusqu'à ce que son admin les pose (setAirdropReceiveGas, setAirdropComposeGas) ; ou leur plafond est sous ce dont une livraison a besoin : l'envoi part quand même, et une livraison qui reste bloquée est réexécutée (plus bas).

Ce qui vaut d'être envoyé

Depuis la septième boucle d'audit, le 2026-10-06, le keeper n'envoie que ce qui vaut ce que son envoi lui coûte. Jusque-là, toute action au-dessus de zéro partait : un don d'actions à peine au-dessus de la poussière au vault d'un marché que personne ne trade faisait ouvrir au keeper le cycle de la fenêtre et payer un message LayerZero, ou un envoi sur Ethereum, chaque jour.

  • Ce qu'un envoi porte. Une action dont le solde se lit et dépasse zéro ; sur le rail du pont, il lui faut aussi un adaptateur et un devis propre au-dessus de zéro. Le devis du vault vaut zéro aussi bien pour la poussière que l'OFT ne transporte pas que pour une action dont l'adaptateur ne peut pas coter l'envoi : depuis la huitième boucle d'audit, le keeper le demande donc à l'adaptateur de l'action lui-même, sur l'envoi que le vault construirait. La poussière n'est jamais envoyée ni signalée ; une action dont le devis échoue, dont l'envoi échouerait, est laissée de côté avec une alerte qui dit pourquoi ; une action que le keeper ne peut pas interroger part comme avant, son envoi tranchant. Jusque-là, une telle action passait pour de la poussière et quittait chaque envoi sans un mot
  • Sa valeur. Chaque action se valorise avec l'oracle de son vault, au dernier prix de son feed, périmé ou non, puisque c'est une estimation et jamais une borne ; les coûts, payés en ETH, passent en dollars par le feed ETH/USD du même oracle
  • Son propre coût. Chaque action doit valoir au moins son propre coût : ses frais LayerZero sur le rail du pont, où chaque action est un message à elle, ou sa part du gas de l'envoi sur le rail local. Un don qui ne vaut pas son propre message ne part jamais avec un vrai envoi
  • L'envoi entier. Les actions qui passent doivent ensemble valoir tout l'envoi, plus l'ouverture du cycle de la fenêtre (openCycle) quand il n'est pas encore ouvert. Sur le rail local, depuis la huitième boucle d'audit, l'ouverture n'est comptée qu'une fois : l'estimation de l'envoi, prise cycle fermé, la contient déjà, et seul le coût de base de la transaction d'ouverture s'y ajoute ; comptée deux fois, elle retenait des jours durant un envoi qui valait un peu plus que son coût. Sinon, aucune ne part, et le cycle n'est pas ouvert
  • Ce qui attend. Ce qui ne part pas reste au vault et part dans une fenêtre suivante, avec ce qui s'accumule. Un marché dont l'envoi attend est compté dans le rapport et noté une fois par fenêtre dans le journal
  • Ce qui ne se lit pas. Un prix, des frais ou un coût de gas que le keeper ne peut pas lire laissent partir l'envoi, comme avant la règle : une lecture qui échoue ne retient jamais un vrai envoi

KEEPER_AIRDROP_MIN_VALUE_BPS fixe le multiple : 10 000, une fois le coût, par défaut, si bien qu'un envoi qui vaut au moins ce qu'il coûte part, quelle que soit sa taille au-delà ; une valeur plus haute retient les petits envois jusqu'à ce qu'ils vaillent ce multiple, et 0 envoie tout ce qui est porté. La même règle dit si le vault a quelque chose à envoyer pour la recherche des actions mises de côté, plus haut. L'audit l'a prise par défaut recommandé ; elle ne décide que du moment où une action part, jamais de combien, de quoi ni d'où.

Le gas des livraisons sur Ethereum

Le 2026-10-06, Sepolia a activé la mise à niveau Glamsterdam d'Ethereum, qui fait coûter à un slot de stockage neuf environ cinq fois son gas d'avant, et chaque livraison de l'airdrop du premier cycle du test LayerZero sur testnet a manqué de gas chez l'exécuteur de LayerZero : un envoi ne portait que le gas de la dernière étape de la livraison, et celui de sa première étape était ce qu'impose le propriétaire de l'adaptateur LayerZero de l'action (l'émetteur, sur mainnet). Depuis la neuvième boucle d'audit, sur la décision du fondateur du même jour :

  • Le keeper simule chaque livraison sur Ethereum avant l'envoi : le contrat du token de l'action qui la reçoit, et le contrat de l'airdrop qui la crédite, tels que la livraison les trouvera. Il demande le besoin de l'action la plus lourde fois 1,25, moins le gas que l'adaptateur LayerZero de l'action impose déjà à la première étape
  • Quand une simulation ne peut pas tourner, il prend ce qu'ont utilisé sur Ethereum les dernières livraisons du marché, puis les valeurs par défaut du hub distant
  • Le hub distant le borne. L'owner de StockFun fixe une valeur par défaut, un plancher et un plafond pour chacune des deux étapes (voir Le rail Robinhood) ; ce que demande le keeper est tenu entre les deux. Une demande que le plafond coupe sous le besoin d'une livraison est « en attente de l'admin » : l'envoi part quand même, et une livraison qui reste bloquée est réexécutée (section suivante)
  • Rien ne change pour les holders. Le keeper ne décide que du gas donné à une livraison, jamais de combien est envoyé, où, ni à qui
  • Choisi de nouveau seulement quand il le faut. Une paire tirée de la simulation de chaque action est gardée pour la fenêtre tant que les actions, l'état du cycle de la fenêtre et la possibilité qu'une livraison arrive après la fin de la fenêtre suivante restent les mêmes ; une paire qu'une simulation n'a pas pu donner est choisie de nouveau à chaque passage. Depuis la dixième boucle d'audit, un envoi qui ne porte que la poussière que le pont ne transporte pas ne lit rien de l'environnement de la livraison, et une paire simulée une fois le cycle de la fenêtre ouvert, un état qui ne revient jamais en arrière, sert de nouveau sans le relire

Une livraison bloquée sur Ethereum

Une livraison à court de gas ne perd rien : elle reste sur l'endpoint de LayerZero sur Ethereum, sa première étape vérifiée et pas exécutée, ou sa dernière étape stockée et pas exécutée, jusqu'à ce que quelqu'un la relance avec plus de gas. Depuis la neuvième boucle d'audit, le keeper le fait lui-même :

  • Il lit à chaque passage l'étape de chaque livraison sur l'endpoint. Une fois le tour de l'exécuteur passé (son échec, cru seulement de l'exécuteur de LayerZero lui-même, ou dix minutes), il relance l'étape bloquée depuis sa clé Ethereum, avec le besoin simulé fois 1,25, et jamais plus que KEEPER_AIRDROP_REEXECUTION_MAX_GAS, 4 000 000 par défaut
  • Une relance qu'il ne peut pas envoyer (sa simulation échoue, son besoin dépasse la borne, sa clé manque d'ETH) est alertée une fois, avec la commande pour la lancer à la main, et retentée à chaque passage
  • Une relance qui échoue de nouveau sur la chaîne, l'étape toujours bloquée, est le second échec : alerté, avec la commande, et jamais renvoyé par le keeper. Il ne le tranche que sur une lecture de l'étape qui répond, et seulement une fois le bloc de la relance échouée enfoncé de quelques blocs (KEEPER_LOG_LAG_BLOCKS), si bien que ni un nœud en retard d'un bloc ni une réorganisation de ce bloc ne peuvent le faire abandonner
  • La clé Ethereum du keeper paie ces relances : sous Glamsterdam, une dernière étape qui ouvre un cycle demande environ un million de gas

Le keeper choisit le moment, et le gas d'une livraison dans les bornes de l'owner, rien d'autre. Un envoi arrivé après l'heure de fermeture suivante est mesuré sur la fenêtre du lendemain.

Le dimensionnement des étapes

Depuis le 2026-10-01, le vault convertit le montant que fixe le keeper, pas son solde entier. Le keeper le dimensionne :

Étape Montant
ETH → USDC Le solde entier, divisé par deux tant que le devis reste sous la borne du vault, jamais sous le seuil de conversion ; depuis le 2026-10-06, le reste attend la fenêtre suivante
Une action sur des pools Uniswap d'Ethereum La réservation de l'action, divisée par deux au plus quatre fois ; une jambe encore en dessous attend le passage suivant, sa réservation intacte
Une action sur le rail Ondo optionnel La réservation de l'action, plafonnée à la limite de notionnel de la session. Depuis le 2026-10-06, une jambe qu'Ondo cote sous la borne du vault n'est jamais envoyée : la cotation gratuite se lit avant toute attestation, et une fois qu'une attestation revient sous la borne, aucune n'est redemandée pour cette action avant la fenêtre suivante
Une action sur Robinhood Chain La réservation de l'action, plafonnée séparément par un plafond fixe et, quand son pool peut être mesuré, par une part de la profondeur de ce pool

Ce qui n'est pas dépensé attend dans le vault un passage suivant (pour l'ETH, la fenêtre suivante) ; le cash d'une action reste réservé à cette action.

Depuis le pipeline de sécurité du 2026-10-01, chaque achat retient n−1 unités sur la dernière action du basket, n étant le nombre d'actions, sur tout rail qui achète des actions. Les vaults donnent le reste d'arrondi des poids à la dernière action : quelques unités de cash arrivées entre la lecture du keeper et sa transaction peuvent baisser cette seule réservation de n−2 unités au plus, et dépenser le montant lu ferait échouer tout l'appel ; n'importe qui pouvait le provoquer par un transfert de poussière. Ce qui est retenu reste réservé pour l'appel suivant. Tout autre appelant des vaults doit garder la même marge. Un correctif dans les contrats, qui change la règle d'allocation documentée, attend la décision de l'owner. Depuis le 2026-10-06, un vault qui ne détient pas plus que ces quelques unités d'USDC (quatre au plus, un basket comptant cinq actions au plus) n'est plus visité pour elles : elles attendent l'USDC suivant que son ETH apporte.

Le burn de $STOCKFUN se dimensionne de la même façon : une tranche dont l'impact de prix dépasse le plafond du keeper est divisée par deux, jamais sous le seuil du keeper, et le reste brûle aux passages suivants. Depuis le pipeline de sécurité du 2026-10-01, une tranche que le pool $STOCKFUN ne peut pas remplir en entier est divisée par deux elle aussi ; avant, le burn échouait pour ce passage. Aucun autre échec n'est retenté.

Ce qu'il ne peut pas faire

Choisir un actif hors basket. Faire passer le cash réservé d'une action à une autre action. Desserrer une borne oracle. Envoyer un actif ailleurs que là où le contrat l'envoie. Retirer quoi que ce soit. Choisir qui reçoit un airdrop ou ce qu'il contient, ou s'attribuer une part.

Les dettes qu'il paie vont toujours au destinataire fixé : ce que le hook doit à un vault ne va qu'à ce vault, ce que le lock garde ne va qu'au vault du pool ou au hook, pour le créateur. Ces appels sont ouverts à tous, et leur résultat ne dépend pas de l'appelant.

Le déclencheur des conversions lui est réservé pour éviter les sandwichs : un déclencheur ouvert à tous pourrait être encadré par les trades d'un tiers.

Le cycle

flowchart TD S[Passage, toutes les 5 minutes] --> U[Veille des transferts d'urgence] U --> AS{Séance US du jour finie ?} AS -->|oui| AD[Airdrop, une fois par fenêtre :

placer, ouvrir le cycle, envoyer] AS -->|non| D AD --> D[Dettes du hook et du lock,

frais de LP une fois par jour] D --> C{Séance US ouverte ?

calendrier NYSE} C -->|non| W[Passage suivant] C -->|oui| P{Contrat en pause ?} P -->|oui| W P -->|non| A{Premier constat du vault

dans la fenêtre : ≥ 0,1 ETH ?} A -->|non, l'ETH attend

la fenêtre suivante| UA{USDC en attente

et à son tour ?} UA -->|non| N[Vault suivant] UA -->|oui| B A -->|oui| E[ETH → USDC, borné 50 bps] E --> B[USDC → USDG → pont] B --> R{Arrivé sur la chaîne distante ?} R -->|non| M[Surveiller, rejouer le ticket] R -->|oui| K[Achat des actions, borné 200 bps,

chaque jambe à part] K --> N

L'intervalle, le seuil et les bornes du diagramme sont les réglages par défaut. L'ETH d'un vault se vérifie une fois par fenêtre, au premier passage après la fermeture, et l'USDC qu'il détient déjà avance une fois après chaque conversion et au plus une fois par fenêtre sinon : c'est son tour. Les livraisons de l'airdrop sont suivies à chaque passage, jour et nuit, et, depuis la sixième boucle d'audit, les tickets du rail canonique aussi.

Sa configuration

Tout passe par des variables d'environnement : RPC des deux chaînes, clé privée du keeper, adresses des contrats, intervalle, et les leviers de sécurité — mode sec, exécution unique, exigence de marché ouvert. Depuis le 2026-10-05 s'y ajoutent :

Variable Par défaut Rôle
KEEPER_AIRDROP true L'étape de l'airdrop
KEEPER_AIRDROP_AFTER_SESSION true N'envoyer qu'après la séance US du jour ; false sur un testnet
KEEPER_AIRDROP_FEE_MARGIN_BPS 1 000 La marge ajoutée aux frais LayerZero d'un envoi, rendue par le vault
KEEPER_AIRDROP_TRANSIT_MINUTES 60 Le délai après lequel une livraison pas exécutée sur Ethereum alerte
KEEPER_COLLECT_FEES true L'encaissement des frais de LP
KEEPER_COLLECT_FEES_HOURS 24 L'intervalle entre deux encaissements d'un pool
KEEPER_COLLECT_FEES_MIN_WEI 0 L'ETH qu'un encaissement doit rapporter pour partir
KEEPER_BRIDGE_ALERT_AFTER_SKIPS 3 Les batches de suite sans un marché avant son alerte
KEEPER_STATE_DIR state Le dossier du fichier d'état
KEEPER_CONVERT_ONCE_PER_WINDOW true Depuis le 2026-10-06 : l'ETH d'un vault se convertit une fois par fenêtre ; depuis la sixième boucle d'audit, son USDC part une fois après chaque conversion et au plus une fois par fenêtre sinon ; false en local et sur testnet, où le keeper convertit, achète et passe le pont à chaque passage
KEEPER_TICKET_ALERT_HOURS 6 Depuis la sixième boucle d'audit, sur le rail canonique : les heures après lesquelles un ticket que le keeper ne sait pas exécuté lève une alerte
KEEPER_AIRDROP_MIN_VALUE_BPS 10 000 Depuis la septième boucle d'audit : ce qu'un envoi à l'airdrop doit valoir face à ce qu'il coûte, en points de base du coût (« Ce qui vaut d'être envoyé », plus haut) ; 0 coupe la règle
KEEPER_LOG_LAG_BLOCKS 2 Depuis la septième boucle d'audit : les blocs sous le dernier bloc d'Ethereum où s'arrêtent ses lectures de journaux ; 0 lit jusqu'au dernier. Depuis la huitième, aussi la profondeur qu'attend le bloc d'un batch de pont avant que le keeper ne le suive
KEEPER_REMOTE_LOG_LAG_BLOCKS 12 Le même retard sur Robinhood Chain ; depuis la huitième boucle d'audit, aussi les blocs sous le dernier où se lit un ticket du rail canonique
KEEPER_AIRDROP_REEXECUTION_MAX_GAS 4 000 000 Depuis la neuvième boucle d'audit : le plus de gas que le keeper donne à une relance d'une livraison bloquée sur Ethereum (« Une livraison bloquée sur Ethereum », plus haut) ; de 1 à 16 777 216
KEEPER_AIRDROP_LZ_EXECUTOR vide Depuis la neuvième boucle d'audit : l'exécuteur de LayerZero sur Ethereum, le seul dont le keeper croit les alertes d'échec ; vide, celui que LayerZero publie pour la chaîne du keeper (mainnet et Sepolia), aucun ailleurs
KEEPER_LOG_MAX_SELECTORS 1 000 Depuis la dixième boucle d'audit : le plus d'adresses et de valeurs d'événement qu'une requête de journaux nomme, comptées comme les comptent les nœuds de Robinhood Chain ; au moins 5. Derrière l'endpoint public de Sepolia, qui refuse dix adresses ou plus, 10

KEEPER_STOCK_ROUTER, le routeur que la factory nomme pour les nouveaux vaults, ne fait plus que choisir la séance qui ouvre un passage, celle d'Ondo pour un routeur Ondo, celle de la NYSE sinon : chaque vault se cote sur son propre routeur.

Depuis la septième boucle d'audit, les identifiants de chaîne, KEEPER_CHAIN_ID et KEEPER_REMOTE_CHAIN_ID, se vérifient sur les RPC avant le premier passage : 1 avec 4663 sur mainnet, 11155111 avec 46630 sur le testnet. Depuis la huitième boucle d'audit, les deux sont requis : KEEPER_CHAIN_ID toujours, KEEPER_REMOTE_CHAIN_ID avec le pont, et le keeper refuse de démarrer sans eux (jusque-là, absent, KEEPER_CHAIN_ID valait 11155111 et KEEPER_REMOTE_CHAIN_ID 4663). Les gardes de l'oracle ne demandent aucun réglage : le keeper les lit sur l'oracle de chaque vault miroir. Une variable entière laissée vide prend sa valeur par défaut, et une valeur hors de sa plage arrête le keeper au démarrage.

La clé du keeper est une clé chaude, financée en gas seulement, distincte de celle du déployeur.

Son état

Ce que le keeper suit d'un passage à l'autre tient, depuis le 2026-10-05, dans un fichier d'état, keeper-state.json : les livraisons de l'airdrop et les transferts du pont en route, les transactions qui attendent leur reçu, les épisodes d'alerte et les séries d'échecs ; depuis la sixième boucle d'audit, aussi les tickets du rail canonique qu'il suit, l'endroit où la recherche de chaque transfert s'est arrêtée, et le moment où l'USDC de chaque vault est parti pour la dernière fois ; depuis la septième, les alertes que son webhook n'a pas encore prises. Un fichier écrit par un keeper plus ancien se charge tel quel. Il est écrit après chaque passage et chaque envoi — un fichier temporaire, synchronisé sur le disque, puis renommé sur le précédent — et lu une fois, au démarrage. Un redémarrage n'oublie donc rien : une livraison qui échoue après un redémarrage est encore signalée, une transaction envoyée avant n'est jamais renvoyée, et un épisode déjà signalé ne l'est pas deux fois. Un fichier illisible, d'une autre version, ou écrit pour une autre factory est mis de côté, renommé, et le keeper repart vide, avec un avertissement. Depuis la huitième boucle d'audit, un fichier écrit pour une autre chaîne reste d'abord tel quel, ni lu ni écrasé, jusqu'à ce que le keeper ait lu la chaîne que sert son RPC : s'il sert la chaîne configurée, le fichier était celui d'une autre chaîne et il est mis de côté alors ; sinon, l'erreur est dans l'identifiant configuré, le keeper refuse de démarrer, et une fois l'identifiant corrigé il reprend son fichier, les dépôts qu'il suivait compris. Jusque-là, un identifiant mal tapé coûtait le fichier avant le refus de démarrer. Les alertes gardées sont, depuis la même boucle, une par alerte distincte ; un fichier écrit par un keeper plus ancien se charge tel quel. Depuis la neuvième boucle d'audit, le fichier garde aussi le paquet de chaque livraison de l'airdrop et ses relances par le keeper, le gas qu'ont utilisé les dernières livraisons du marché, et le dernier rejeu par le keeper de chaque ticket du rail canonique ; un fichier écrit par un keeper plus ancien se charge toujours tel quel. Le mode sec lit le fichier sans jamais l'écrire.

Chaque transaction n'est envoyée qu'une fois. Un reçu qui ne se lit pas laisse la transaction en attente, relue par son hash aux passages suivants ; tant qu'elle n'est pas lue, rien de la même sorte ne repart pour le même vault, le même marché ou le même ticket, ni aucun nouveau batch de pont.

Depuis le 2026-10-06 :

  • Chaque envoi est sur le disque avant l'attente. Chaque transaction part au nonce que la chaîne donne à l'adresse du keeper juste avant l'envoi, et s'inscrit dans le fichier d'état, avec son hash et son nonce, dès que le hash revient, avant que son reçu soit attendu : un keeper tué pendant l'attente la relit par son hash à son redémarrage au lieu de la renvoyer
  • Une transaction perdue est abandonnée. Une transaction qu'aucun nœud ne connaît plus quatre fois KEEPER_RECEIPT_ALERT_MS après son envoi (deux heures par défaut) est abandonnée avec une alerte, et ne retient plus les envois de sa sorte ; son nonce reste libre, et la transaction suivante du keeper le prend. L'alerte d'une transaction toujours pas minée dit comment la remplacer
  • Écrit en entier. Un disque trop plein pour prendre le fichier est une erreur, alertée, et le dernier bon fichier reste ; jusque-là, une copie tronquée pouvait le remplacer
  • Un keeper par dossier. Le keeper prend un verrou dans son dossier d'état (keeper.lock) : un second keeper sur le même dossier refuse de démarrer et nomme le premier. Le verrou d'un keeper disparu est repris ; celui qu'a laissé un keeper d'une autre machine ne l'est pas, et l'opérateur le supprime une fois ce keeper arrêté. Un mode sec ne prend pas de verrou

Ses limites connues

  • La veille des transferts d'urgence ne garde pas sa position : un keeper redémarré part de la tête de chaîne qu'il lit, et les événements de son arrêt ne sont pas lus
  • Sur le rail USDG, une livraison refusée avant le démarrage du keeper, ou pendant son arrêt, n'apparaît que comme un avertissement du sweep ; le rail canonique la voit dans l'état du hub distant
  • Jusqu'à la neuvième boucle d'audit, il ne relançait pas lui-même une livraison d'airdrop qui avait échoué sur Ethereum : son alerte donnait la commande lzCompose. Depuis, il relance une étape bloquée, dans sa borne, et un second échec est laissé à une personne, avec la commande
  • Un keeper tué dans les quelques millisecondes entre un envoi et son inscription dans le fichier d'état peut renvoyer cette transaction une fois à son redémarrage ; les contrôles des contrats font échouer la plupart de ces doublons, au prix de leur gas
  • La recherche de l'arrivée d'un transfert ne relit jamais un bloc : une réorganisation qui déplace une arrivée dans un bloc déjà lu laisse ce transfert à l'alerte de transit. Depuis la septième boucle d'audit, chaque lecture de journaux s'arrête quelques blocs sous le dernier, ce qui retarde d'autant un crédit, et son alerte de transit
  • Depuis la huitième boucle d'audit, un batch de pont est suivi une fois son bloc enfoncé de quelques blocs, un passage plus tard qu'avant, et le batch suivant l'attend ; une réorganisation plus profonde n'est pas couverte, comme pour les lectures de journaux. Un ticket exécuté dans les tout derniers blocs se lit encore vivant à ce bloc, jusqu'à ce que le dernier bloc ait dépassé son exécution d'autant de blocs : le passage suivant sur une chaîne active, plusieurs sur une chaîne calme. Depuis la neuvième boucle d'audit, celui que le rejeu du keeper lui-même a supprimé n'est ni rejoué de nouveau ni alerté entre-temps, après un redémarrage aussi ; celui qu'un autre a rejoué dans ces blocs est retenté, échoue en simulation, et peut encore être alerté comme vivant passé les heures de l'alerte
  • La minute pendant laquelle le keeper lit au bloc de sa propre dernière transaction ne couvre pas un nœud en retard de plus d'une minute sur ses pairs
  • Sur le rail local, l'ouverture de la fenêtre n'est comptée qu'une fois, sans ce que la seconde transaction refait (quelque 5 % du coût) : un envoi peut partir en valant jusqu'à autant de moins que ce qu'il coûte
  • Depuis la dixième boucle d'audit, une action donnée à un vault après la clôture, hors de tout achat, part avec l'envoi de la séance suivante, dans une fenêtre plus tardive : le keeper ne relit un vault sans rien à envoyer qu'à la séance suivante. Un achat dont le keeper a lu le reçu au dernier passage de la séance pourrait aussi se lire vide, au premier passage après la clôture, sur un nœud en retard sur lui, ce qui demande un intervalle entre passages plus court que le retard de ce nœud (environ cinq secondes sur Sepolia), jamais les cinq minutes par défaut

Le préflight

Avant tout lancement réel, une commande de préflight vérifie sans écrire une seule transaction : que les réseaux répondent, que les adresses portent du code, que les peers LayerZero de l'OFT USDG se correspondent, qu'USDG n'est pas mis en pause par son émetteur, Paxos, que les poids des baskets sont ceux attendus.

Depuis la huitième boucle d'audit, elle vérifie aussi les gardes de l'oracle. Son manifeste doit dire comment le contrôle du séquenceur est réglé, dans un sens ou dans l'autre : ni feed ni délai de grâce (le contrôle éteint, comme aujourd'hui), ou un feed avec un délai. Elle lit ce feed quand il y en a un, et la pause d'oracle du token de chaque action : un token en pleine opération sur titres est un avertissement, qui ne fait pas échouer la vérification ; un token sans le signal la fait échouer. Après le déploiement, elle vérifie que l'oracle déployé suit le manifeste, que l'état de son séquenceur laisse passer les prix, et que la pause d'oracle de chaque action est allumée. La commande inspect du keeper imprime les mêmes gardes : le contrôle du séquenceur et son état, et pour chaque action sa pause d'oracle et si son token est en pause maintenant.

Depuis la neuvième boucle d'audit, elle tourne aussi sur le déploiement du test LayerZero sur testnet (Sepolia et le testnet de Robinhood Chain), à partir des deux fichiers de ce test, avec des RPC nommés pour cette paire, si bien qu'un RPC de mainnet n'est jamais interrogé sur le testnet ; toute autre paire de chaînes, un mélange compris, est refusée. Sur le testnet, elle ne lit aucun routeur Uniswap v3, que cette chaîne n'a pas, et tient le feed ETH/USD au heartbeat donné à l'oracle du test. Sur les deux paires, elle vérifie que le routeur distant nomme le routeur v3 que dit le manifeste. Lancée sur le déploiement du testnet : 68 vérifications, toutes passées.

Elle refuse de tourner sans RPC plutôt que de rapporter un faux succès.