Aller au contenu principal
ingénieriesécuritéapi
Concevoir un trading non-custodial : périmètres des clés API et ne jamais toucher aux retraits

Concevoir un trading non-custodial : périmètres des clés API et ne jamais toucher aux retraits

« Non-custodial » est le plus souvent un mot marketing. Employé avec précision, c'est une contrainte architecturale, et les contraintes de ce type sont intéressantes parce qu'elles suppriment du travail au lieu d'en ajouter.

Un bot de trading doit passer des ordres en votre nom. Les exchanges le rendent possible grâce aux clés API, et les clés API portent des périmètres : lecture, trade, et sur la plupart des plateformes, retrait. La question de conception n'est donc pas de savoir s'il faut demander des permissions. C'est lesquelles, et quoi faire quand vous en recevez plus que ce que vous avez demandé.

Points clés

  • Promettre de ne pas utiliser une permission est plus faible que de ne pas l'avoir. Nous refusons les clés porteuses d'une permission de retrait plutôt que de les accepter et de les ignorer.
  • Refuser est une garantie plus forte que faire confiance, parce que cela ne dépend pas de la correction de notre code ni de l'honnêteté de notre équipe.
  • Si un flux de connexion écrit dans le compte de l'utilisateur, ces écritures doivent venir après la décision d'accepter, pas avant. Sinon vous modifiez les réglages de quelqu'un sur le chemin qui mène à lui dire non.
  • Ne pas détenir les fonds supprime toute une classe de problèmes difficiles : pas de registre de custody, pas de réconciliation, pas de file d'attente de retraits.
  • Ce qui la remplace est plus étroit et plus tranchant : votre système doit être correct en temps réel, parce qu'il ne peut jamais rien annuler.

1. Refuser une permission vaut mieux que promettre de ne pas l'utiliser

L'implémentation évidente consiste à demander aux utilisateurs une clé avec les périmètres lecture et trade, et à simplement ne jamais appeler les endpoints de retrait. La majeure partie du secteur fait ainsi, et ce n'est pas déraisonnable.

Nous faisons quelque chose de plus strict. Si une clé arrive porteuse d'une permission de retrait, elle est refusée. L'utilisateur voit un message lui indiquant exactement quoi faire :

Cette clé API a la permission de retrait activée. Freya ne retire jamais de fonds, c'est pourquoi les clés portant cette permission ne sont pas acceptées. Veuillez créer une clé avec Lecture et Trade uniquement.

La même règle s'applique sur les trois exchanges que nous prenons en charge. Il n'y a ni dérogation, ni exception administrateur.

Au premier abord, cela ressemble à de la friction gratuite. La clé fonctionnerait. L'utilisateur s'est donné du mal pour la créer. La rejeter revient à le renvoyer vers l'exchange pour en fabriquer une autre.

La raison, c'est que les deux conceptions font des promesses différentes, et que la différence ne devient visible que le pire jour.

  • « Nous n'appelons jamais les endpoints de retrait » est une affirmation sur notre comportement. Elle tient tant que notre code est correct, que nos dépendances sont saines et que toute personne ayant accès au déploiement reste honnête. Ce sont des attentes raisonnables. Ce ne sont pas des choses qu'un utilisateur peut vérifier.
  • « La clé ne peut pas retirer » est une affirmation sur la clé. Elle tient quoi que fasse notre code, parce que c'est l'exchange qui l'applique, et l'utilisateur peut le confirmer lui-même dans ses propres réglages d'exchange.

La seconde est une demande plus faible et une garantie plus forte. Préférez les conceptions dont la propriété de sécurité est appliquée par quelqu'un d'autre que vous, surtout lorsque l'utilisateur n'a aucun moyen d'auditer votre côté.

Il y a un bénéfice plus modeste qui compte en pratique : cela rend le mode d'échec bruyant. Une clé au périmètre trop large est attrapée au moment de la connexion, devant un utilisateur en train de tout configurer et qui peut corriger en une minute. L'alternative, c'est que la clé au périmètre excessif reste là à fonctionner très bien, et que personne n'apprenne rien jusqu'à ce que cela compte.

2. Dites non avant de toucher à quoi que ce soit

Voici le détail que je voudrais le plus voir un autre ingénieur retenir, parce qu'il est facile à rater et quasi invisible quand on le rate.

Connecter un exchange n'est pas une opération purement en lecture. Selon la plateforme, amener un compte dans un état exploitable peut impliquer d'y écrire : ajuster un mode de compte, définir un mode de position, et ainsi de suite. Ce sont des étapes légitimes qui font partie de la configuration.

Considérez maintenant l'ordre des opérations. Si la vérification de permission a lieu après ces écritures, alors une clé rejetée laisse quand même une trace. L'utilisateur vous a confié un identifiant, vous avez modifié des réglages dans son compte, puis vous lui avez dit non. De son côté, rien n'explique pourquoi la configuration de son exchange diffère de l'état dans lequel il l'avait laissée.

D'où la règle sur laquelle nous nous sommes arrêtés : un identifiant refusé doit laisser le compte de l'utilisateur exactement tel qu'il a été trouvé. Toute étape qui écrit appartient à l'après-décision d'accepter, jamais à l'avant. Le rejet doit être atteignable sans effets de bord.

Généralisée, ce n'est que de la pensée transactionnelle appliquée au système de quelqu'un d'autre. Vous ne pouvez pas annuler une écriture dans un compte tiers, donc l'ordre des opérations est votre seule protection. Quand vous ne pouvez pas annuler, séquencez avec soin.

Une note d'implémentation qui nous a épargné toute une catégorie de bugs : le refus est exprimé comme un résultat invalide plutôt que comme un drapeau spécial, si bien que chaque chemin qui consomme un résultat de validation le gère sans avoir à savoir qu'il existe. Nous avons deux flux de connexion, et tous deux héritent du même refus parce qu'aucun d'eux n'a eu à penser à le demander. Encoder une règle dans un type que tous les consommateurs lisent déjà vaut mieux que la documenter et espérer.

3. Ce que la contrainte supprime

C'est ici que l'échange devient nettement favorable.

Les fonds ne quittent jamais le compte d'exchange de l'utilisateur. Les bots tradent avec des clés lecture-et-trade, et le solde reste là où il a commencé. Il en découle une longue liste de choses qui, tout simplement, n'existent pas dans notre système :

  • Pas de registre de custody, donc pas de réconciliation entre un solde interne et un solde externe
  • Pas de file d'attente de retraits, pas de workflow d'approbation, pas de limites quotidiennes sur le capital de trading
  • Pas de séparation entre hot wallet et cold wallet pour ce capital, et pas de gestion de clés autour
  • Pas de question sur ce qu'il advient des fonds des utilisateurs si l'entreprise disparaît

Chacun de ces éléments est un sous-système entier dans un produit custodial, et chacun est un endroit où l'argent peut disparaître. Ils sont absents ici non pas parce que nous les avons résolus, mais parce que nous n'avons jamais pris en charge la chose qui les crée.

Je tiens toutefois à être précis plutôt que catégorique : cela s'applique au capital de trading. Les soldes de plateforme utilisés pour les abonnements sont une affaire distincte, avec leur propre traitement. La propriété non-custodial concerne l'argent qui trade, et c'est l'argent qui compte le plus.

4. Ce que cela ne corrige pas

Une contrainte qui supprime des problèmes supprime aussi les outils dont vous vous serviriez pour corriger les erreurs, et il vaut la peine d'être honnête à ce sujet.

Les trades sont définitifs. Avec la custody, un transfert interne peut en principe être annulé. Un ordre exécuté sur le compte d'exchange de quelqu'un d'autre ne le peut pas. Tout le poids de l'ingénierie se déplace vers le fait d'être correct avant que la requête ne parte, plutôt que de réconcilier après qu'elle a atterri. C'est une discipline plus difficile qu'il n'y paraît, et c'est pourquoi une grande partie de notre travail porte sur la validation, la précision et l'idempotence.

La permission de trade reste une vraie permission. Une clé lecture-et-trade divulguée ne peut pas vider un compte vers une adresse externe, ce qui est le résultat que les gens redoutent réellement. Mais elle peut passer des ordres. Le rayon d'impact est borné, pas nul, et il serait malhonnête de le décrire comme nul.

L'utilisateur conserve des obligations. La rotation des clés, le fait de ne pas réutiliser les clés entre services, et de surveiller ce qui d'autre a accès au même compte lui incombent toujours. Notre architecture borne les dégâts ; elle ne rend pas la clé inoffensive.

Ce que je dirais à une autre équipe

  • Demandez le périmètre le plus étroit, puis vérifiez que vous l'avez obtenu. Demander moins n'est pas la même chose que recevoir moins.
  • Préférez les garanties appliquées en dehors de votre propre code, en particulier celles que l'utilisateur peut vérifier sans vous faire confiance.
  • Rendez le rejet sans effet de bord. Si votre flux écrit dans un compte tiers, tout ce qui écrit passe après la décision d'accepter.
  • Encodez la règle là où les consommateurs regardent déjà. Une règle exprimée dans un type partagé n'a pas besoin que chaque appelant s'en souvienne.

Le point le plus large, c'est que « nous ne ferons pas X » et « nous ne pouvons pas faire X » se lisent de façon similaire sur une landing page et sont complètement différents lors d'un incident. Chaque fois que vous pouvez faire passer une promesse de la première catégorie à la seconde, le coût est généralement faible et le gain apparaît précisément quand tout le reste va de travers.


Pour aller plus loin

Créer un compte

Mettez vos connaissances en pratique et ouvrez un compte Freya dès aujourd'hui.

S'inscrire maintenant
Ce contenu est uniquement à des fins éducatives et ne constitue pas un conseil financier. Le trading de cryptomonnaies comporte des risques significatifs. N'investissez jamais plus que ce que vous pouvez vous permettre de perdre.