Visite guidée du contrat Uniswap-v2
Introduction
Uniswap v2 (opens in a new tab) peut créer un marché d'échange entre deux jetons ERC-20, quels qu'ils soient. Dans cet article, nous allons passer en revue le code source des contrats qui implémentent ce protocole et voir pourquoi ils sont écrits comme ils le sont.
Que fait Uniswap ?
Fondamentalement, il existe deux types d'utilisateurs : les fournisseurs de liquidités et les traders.
Les fournisseurs de liquidités fournissent le pool avec les deux jetons qui peuvent être échangés (nous les appellerons Jeton0 et Jeton1). En retour, ils reçoivent un troisième jeton qui représente une propriété partielle du pool, appelé un jeton de liquidité.
Les traders envoient un type de jeton au pool et reçoivent l'autre (par exemple, envoient le Jeton0 et reçoivent le Jeton1) hors du pool fourni par les fournisseurs de liquidités. Le taux de change est déterminé par le nombre relatif de Jeton0 et de Jeton1 que le pool possède. En outre, le pool prélève un petit pourcentage en guise de récompense pour le pool de liquidités.
Lorsque les fournisseurs de liquidités veulent récupérer leurs actifs, ils peuvent brûler les jetons du pool et recevoir en retour leurs jetons, y compris leur part des récompenses.
Cliquez ici pour une description plus complète (opens in a new tab).
Pourquoi v2 ? Pourquoi pas v3 ?
Uniswap v3 (opens in a new tab) est une mise à niveau bien plus compliquée que la v2. Il est plus facile d'apprendre d'abord la v2, puis de passer à la v3.
Contrats de base et contrats périphériques
Uniswap v2 est divisé en deux composants, un noyau et une périphérie. Cette division permet aux contrats de base, qui détiennent les actifs et doivent donc être sécurisés, d'être plus simples et plus faciles à auditer. Toutes les fonctionnalités supplémentaires requises par les traders peuvent alors être fournies par des contrats périphériques.
Flux de données et de contrôle
Voici le flux de données et de contrôle qui se produit lorsque vous effectuez les trois actions principales d'Uniswap :
- Échanger entre différents jetons
- Ajouter de la liquidité au marché et être récompensé par des jetons de liquidité ERC-20 d'échange de paires
- Brûler des jetons de liquidité ERC-20 et récupérer les jetons ERC-20 que l'échange de paires permet aux traders d'échanger
Échange
C'est le flux le plus courant, utilisé par les traders :
Appelant
- Fournir au compte périphérique une provision pour le montant à échanger.
- Appeler l'une des nombreuses fonctions d'échange du contrat périphérique (la fonction utilisée dépend de si l'ETH est impliqué ou non, si le trader spécifie le montant de jetons à déposer ou le montant de jetons à récupérer, etc.).
Chaque fonction d'échange accepte un
path(chemin), un tableau d'échanges à parcourir.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Identifier les montants qui doivent être échangés sur chaque échange le long du chemin.
- Itère sur le chemin. Pour chaque échange le long du chemin, il envoie le jeton d'entrée, puis appelle la fonction
swapde l'échange. Dans la plupart des cas, l'adresse de destination des jetons est le prochain échange de paires sur le chemin. Pour l'échange final, il s'agit de l'adresse fournie par le trader.
Dans le contrat de base (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2}5. Vérifier que le contrat de base ne fait pas l'objet d'une tricherie et peut maintenir une liquidité suffisante après l'échange.
- Voir combien de jetons supplémentaires ont été reçus en plus des réserves connues. Ce montant correspond au nombre de jetons d'entrée reçus pour l'échange.
- Envoyer les jetons de sortie à la destination.
- Appeler
_updatepour mettre à jour les montants de la réserve.
Retour dans le contrat périphérique (UniswapV2Router02.sol)
- Effectuer tout nettoyage nécessaire (par exemple, brûler des jetons WETH pour récupérer des ETH à envoyer au trader).
Ajout de liquidité
Appelant
- Fournir au compte périphérique une provision pour les montants à ajouter au pool de liquidités.
- Appeler l'une des fonctions
addLiquiditydu contrat périphérique.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Créer un nouvel échange de paires si nécessaire.
- S'il existe un échange de paires, calculer le montant des jetons à ajouter. La valeur est censée être identique pour les deux jetons, donc le ratio des nouveaux jetons par rapport aux jetons existants doit être le même.
- Vérifier si les montants sont acceptables (les appelants peuvent spécifier un montant minimum en dessous duquel ils préfèrent ne pas ajouter de liquidité).
- Appeler le contrat de base.
Dans le contrat de base (UniswapV2Pair.sol)
- Frapper des jetons de liquidité et les envoyer à l'appelant.
- Appeler
_updatepour mettre à jour les montants de la réserve.
Retrait de liquidité
Appelant
- Fournir au compte périphérique une provision de jetons de liquidité à brûler en échange des jetons sous-jacents.
- Appeler l'une des fonctions
removeLiquiditydu contrat périphérique.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Envoyer les jetons de liquidité à l'échange de paires.
Dans le contrat de base (UniswapV2Pair.sol)
- Envoyer à l'adresse de destination les jetons sous-jacents en proportion des jetons brûlés. Par exemple, s'il y a 1 000 jetons A dans le pool, 500 jetons B et 90 jetons de liquidité, et que nous recevons 9 jetons à brûler, nous brûlons 10 % des jetons de liquidité, nous renvoyons donc à l'utilisateur 100 jetons A et 50 jetons B.
- Brûler les jetons de liquidité.
- Appeler
_updatepour mettre à jour les montants de la réserve.
Les contrats de base
Ce sont les contrats sécurisés qui détiennent la liquidité.
UniswapV2Pair.sol
Ce contrat (opens in a new tab) met en œuvre le pool réel qui échange les jetons. C'est la fonctionnalité principale d'Uniswap.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';Afficher toutCe sont toutes les interfaces que le contrat doit connaître, soit parce que le contrat les implémente (IUniswapV2Pair et UniswapV2ERC20), soit parce qu'il appelle des contrats qui les implémentent.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {Ce contrat hérite d'UniswapV2ERC20, qui fournit les fonctions ERC-20 pour les jetons de liquidité.
1 using SafeMath for uint;La bibliothèque SafeMath (opens in a new tab) est utilisée pour éviter les dépassements de capacité (overflow) et les sous-dépassements (underflow). Ceci est important car sinon, nous pourrions nous retrouver dans une situation où une valeur devrait être -1, mais est en fait 2^256-1.
1 using UQ112x112 for uint224;De nombreux calculs dans le contrat de pool nécessitent des fractions. Cependant, les fractions ne sont pas prises en charge par l'EVM.
La solution trouvée par Uniswap est d'utiliser des valeurs de 224 bits, avec 112 bits pour la partie entière et 112 bits pour la fraction. Ainsi, 1,0 est représenté par 2^112, 1,5 est représenté par 2^112 + 2^111, etc.
Plus de détails sur cette bibliothèque sont disponibles plus loin dans le document.
Variables
1 uint public constant MINIMUM_LIQUIDITY = 10**3;Pour éviter les cas de division par zéro, il existe toujours un nombre minimum de jetons de liquidité (mais ils sont détenus par le compte zéro). Ce nombre est MINIMUM_LIQUIDITY, soit un millier.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));C'est le sélecteur ABI pour la fonction de transfert ERC-20. Il est utilisé pour transférer des jetons ERC-20 sur les deux comptes de jetons.
1 address public factory;C'est le contrat d'usine qui a créé ce pool. Chaque pool est un échange entre deux jetons ERC-20, l'usine est le point central qui relie tous ces pools.
1 address public token0;2 address public token1;Ce sont les adresses des contrats pour les deux types de jetons ERC-20 qui peuvent être échangés par ce pool.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReservesLa réserve du pool pour chaque type de jeton. Nous supposons que les deux représentent le même montant de valeur, et donc que chaque jeton0 vaut la valeur de jeton1 multipliée par reserve1/reserve0.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReservesL'horodatage du dernier bloc dans lequel un échange a eu lieu, utilisé pour suivre les taux de change dans le temps.
L'une des plus grandes dépenses de gaz des contrats Ethereum est le stockage, qui persiste d'un appel de contrat à l'autre. Chaque cellule de stockage fait 256 bits de long. Ainsi, trois variables, reserve0, reserve1 et blockTimestampLast, sont allouées de manière à ce qu'une seule valeur de stockage puisse inclure les trois (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;Ces variables contiennent les coûts cumulatifs pour chaque jeton (chacun en fonction de l'autre). Elles peuvent être utilisées pour calculer le taux de change moyen sur une période de temps.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity eventPour décider du taux de change entre jeton0 et jeton1, l'échange de paires maintient le multiple des deux réserves constant pendant les transactions. kLast est cette valeur. Elle change lorsqu'un fournisseur de liquidités dépose ou retire des jetons, et elle augmente légèrement en raison des frais de marché de 0,3 %.
Voici un exemple simple. Notez que par souci de simplicité, le tableau ne comporte que trois chiffres après la virgule, et que nous ignorons les frais de transaction de 0,3 %, de sorte que les chiffres ne sont pas exacts.
| Événement | réserve0 | réserve1 | réserve0 * réserve1 | Taux de change moyen (jeton1 / jeton0) |
|---|---|---|---|---|
| Configuration initiale | 1 000,000 | 1 000,000 | 1 000 000 | |
| Le trader A échange 50 jetons0 contre 47,619 jetons1 | 1 050,000 | 952,381 | 1 000 000 | 0,952 |
| Le trader B échange 10 jetons0 contre 8,984 jetons1 | 1 060,000 | 943,396 | 1 000 000 | 0,898 |
| Le trader C échange 40 jetons0 contre 34,305 jetons1 | 1 100,000 | 909,090 | 1 000 000 | 0,858 |
| Le trader D échange 100 jetons1 contre 109,01 jetons0 | 990,990 | 1 009,090 | 1 000 000 | 0,917 |
| Le trader E échange 10 jetons0 contre 10,079 jetons1 | 1 000,990 | 999,010 | 1 000 000 | 1,008 |
Plus les traders fournissent de jetons0, plus la valeur relative du jeton1 augmente, et vice versa, en fonction de l'offre et de la demande.
Verrouillage
1 uint private unlocked = 1;Il existe une catégorie de vulnérabilités de sécurité basées sur l'abus de réentrance (opens in a new tab). Uniswap doit transférer des jetons ERC-20 arbitraires, ce qui signifie appeler des contrats ERC-20 qui pourraient tenter d'abuser du marché Uniswap qui les appelle.
En intégrant une variable unlocked au contrat, nous pouvons empêcher l'appel de fonctions pendant qu'elles sont en cours d'exécution (dans la même transaction).
1 modifier lock() {Cette fonction est un modificateur (opens in a new tab), une fonction qui enveloppe une fonction normale pour en modifier le comportement d'une manière ou d'une autre.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;Si unlocked est égal à un, le définir à zéro. S'il est déjà à zéro, annuler l'appel, le faire échouer.
1 _;Dans un modificateur, _; est l'appel de fonction d'origine (avec tous les paramètres). Ici, cela signifie que l'appel de fonction ne se produit que si unlocked valait un lorsqu'il a été appelé, et que pendant son exécution, la valeur de unlocked est nulle.
1 unlocked = 1;2 }Une fois que la fonction principale est retournée, libérer le verrou.
Divers fonctions
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }Cette fonction fournit aux appelants l'état actuel de l'échange. Notez que les fonctions Solidity peuvent retourner plusieurs valeurs (opens in a new tab).
1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));Cette fonction interne transfère un montant de jetons ERC20 de l'échange à une autre personne. SELECTOR spécifie que la fonction que nous appelons est transfer(address,uint) (voir la définition ci-dessus).
Pour éviter d'avoir à importer une interface pour la fonction de jeton, nous créons « manuellement » l'appel en utilisant l'une des fonctions ABI (opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }Un appel de transfert ERC-20 peut signaler un échec de deux manières :
- Annuler. Si un appel à un contrat externe est annulé, alors la valeur de retour booléenne est
false. - Se terminer normalement mais signaler un échec. Dans ce cas, le tampon de valeur de retour a une longueur non nulle et, lorsqu'il est décodé en tant que valeur booléenne, il est
false.
Si l'une de ces conditions se produit, annuler.
Événements
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);Ces deux événements sont émis lorsqu'un fournisseur de liquidités dépose des liquidités (Mint) ou les retire (Burn). Dans les deux cas, les montants des jetons0 et jetons1 déposés ou retirés font partie de l'événement, tout comme l'identité du compte qui nous a appelés (sender). Dans le cas d'un retrait, l'événement inclut également la cible qui a reçu les jetons (to), qui peut ne pas être la même que l'expéditeur.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );Cet événement est émis lorsqu'un trader échange un jeton contre un autre. Encore une fois, l'expéditeur et le destinataire peuvent ne pas être les mêmes. Chaque jeton peut être soit envoyé à l’échange, soit reçu de celui-ci.
1 event Sync(uint112 reserve0, uint112 reserve1);Enfin, Sync est émis chaque fois que des jetons sont ajoutés ou retirés, quelle qu'en soit la raison, pour fournir les dernières informations de réserve (et donc le taux de change).
Fonctions de configuration
Ces fonctions sont censées être appelées une fois, lors de la mise en place du nouvel échange de paires.
1 constructor() public {2 factory = msg.sender;3 }Le constructeur s'assure que nous garderons une trace de l'adresse de l'usine qui a créé la paire. Ces informations sont requises pour initialize et pour les frais d'usine (s'il y en a).
1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }Cette fonction permet à l'usine (et seulement à l'usine) de spécifier les deux jetons ERC-20 que cette paire échangera.
Fonctions de mise à jour interne
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {Cette fonction est appelée chaque fois que des jetons sont déposés ou retirés.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');Si balance0 ou balance1 (uint256) est supérieur à uint112(-1) (=2^112-1) (donc il déborde et revient à 0 lorsqu'il est converti en uint112), refuser de continuer le _update pour éviter les débordements. Avec un jeton normal qui peut être subdivisé en 10^18 unités, cela signifie que chaque échange est limité à environ 5,1*10^15 de chaque jeton. Jusqu'à présent, cela n'a pas posé de problème.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {Si le temps écoulé n'est pas nul, cela signifie que nous sommes la première transaction d'échange sur ce bloc. Dans ce cas, nous devons mettre à jour les accumulateurs de coûts.
1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }Chaque accumulateur de coûts est mis à jour avec le coût le plus récent (réserve de l'autre jeton/réserve de ce jeton) multiplié par le temps écoulé en secondes. Pour obtenir un prix moyen, vous lisez le prix cumulé à deux moments donnés et vous divisez par la différence de temps entre eux. Par exemple, supposons la séquence d'événements suivante :
| Événement | réserve0 | réserve1 | horodatage | Taux de change marginal (réserve1 / réserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Configuration initiale | 1 000,000 | 1 000,000 | 5 000 | 1,000 | 0 |
| Le trader A dépose 50 jetons0 et récupère 47,619 jetons1 | 1 050,000 | 952,381 | 5 020 | 0,907 | 20 |
| Le trader B dépose 10 jetons0 et récupère 8,984 jetons1 | 1 060,000 | 943,396 | 5 030 | 0,890 | 20+10*0,907 = 29,07 |
| Le trader C dépose 40 jetons0 et récupère 34,305 jetons1 | 1 100,000 | 909,090 | 5 100 | 0,826 | 29,07+70*0,890 = 91,37 |
| Le trader D dépose 100 jetons1 et récupère 109,01 jetons0 | 990,990 | 1 009,090 | 5 110 | 1,018 | 91,37+10*0,826 = 99,63 |
| Le trader E dépose 10 jetons0 et récupère 10,079 jetons1 | 1 000,990 | 999,010 | 5 150 | 0,998 | 99,63+40*1,1018 = 143,702 |
Disons que nous souhaitons calculer le prix moyen du Jeton0 entre les horodatages 5 030 et 5 150. La différence de la valeur de price0Cumulative est 143,702-29,07=114,632. Il s'agit de la moyenne sur deux minutes (120 secondes). Le prix moyen est donc 114,632/120 = 0,955.
Ce calcul de prix est la raison pour laquelle nous devons connaître les anciennes tailles de réserve.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }Enfin, mettre à jour les variables globales et émettre un événement Sync.
_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {Dans Uniswap 2.0, les traders paient des frais de 0,30 % pour utiliser le marché. La plus grande partie de ces frais (0,25 % de la transaction) va toujours aux fournisseurs de liquidités. Les 0,05 % restants peuvent être versés soit aux fournisseurs de liquidités, soit à une adresse spécifiée par l'usine en tant que frais de protocole, qui rémunère Uniswap pour ses efforts de développement.
Pour réduire les calculs (et donc les frais de gaz), ces frais ne sont calculés que lorsque la liquidité est ajoutée ou retirée du pool, plutôt qu'à chaque transaction.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);Lire la destination des frais de l'usine. S'ils sont nuls, alors il n'y a pas de frais de protocole et il n'est pas nécessaire de les calculer.
1 uint _kLast = kLast; // gas savingsLa variable d'état kLast est située dans le stockage, elle aura donc une valeur entre les différents appels au contrat.
L'accès au stockage est beaucoup plus cher que l'accès à la mémoire volatile qui est libérée lorsque l'appel de fonction au contrat se termine. Nous utilisons donc une variable interne pour économiser du gaz.
1 if (feeOn) {2 if (_kLast != 0) {Les fournisseurs de liquidités obtiennent leur part simplement par l'appréciation de leurs jetons de liquidité. Mais les frais de protocole nécessitent que de nouveaux jetons de liquidité soient frappés et fournis à l'adresse feeTo.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {S'il y a de nouvelles liquidités sur lesquelles percevoir des frais de protocole. Vous pouvez voir la fonction de racine carrée plus loin dans cet article.
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;Ce calcul compliqué des frais est expliqué dans le livre blanc (opens in a new tab) à la page 5. Nous savons qu'entre le moment où kLast a été calculé et le moment présent, aucune liquidité n'a été ajoutée ou supprimée (car nous exécutons ce calcul à chaque fois que la liquidité est ajoutée ou supprimée, avant qu'elle ne change réellement). Par conséquent, tout changement dans reserve0 * reserve1 doit provenir des frais de transaction (sans eux, nous conserverions la constante reserve0 * reserve1).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }Utiliser la fonction UniswapV2ERC20._mint pour créer les jetons de liquidité supplémentaires et les affecter à feeTo.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }S'il n'y a pas de frais, régler kLast à zéro (si ce n'est pas déjà le cas). Lorsque ce contrat a été rédigé, il existait une fonctionnalité de remboursement de gaz (opens in a new tab) qui encourageait les contrats à réduire la taille globale de l'état d'Ethereum en mettant à zéro le stockage dont ils n'avaient pas besoin.
Ce code obtient ce remboursement lorsque c'est possible.
Fonctions accessibles en externe
Notez que bien que toute transaction ou tout contrat puisse appeler ces fonctions, elles sont conçues pour être appelées depuis le contrat périphérique. Si vous les appelez directement, vous ne pourrez pas tricher sur l'échange de paires, mais vous pourriez perdre de la valeur par erreur.
frapper
1 // this low-level function should be called from a contract which performs important safety checks2 function mint(address to) external lock returns (uint liquidity) {Cette fonction est appelée lorsqu'un fournisseur de liquidités ajoute de la liquidité au pool. Elle frappe des jetons de liquidité supplémentaires en guise de récompense. Elle devrait être appelée à partir d'un contrat périphérique qui l'appelle après avoir ajouté la liquidité dans la même transaction (afin que personne d'autre ne puisse soumettre une transaction réclamant la nouvelle liquidité avant le propriétaire légitime).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsC'est ainsi qu'il faut lire les résultats d'une fonction Solidity qui retourne plusieurs valeurs. Nous ignorons la dernière valeur retournée, l'horodatage du bloc, car nous n'en avons pas besoin.
1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1);Obtenir les soldes actuels et voir combien a été ajouté pour chaque type de jeton.
1 bool feeOn = _mintFee(_reserve0, _reserve1);Calculer les frais de protocole à percevoir, le cas échéant, et frapper les jetons de liquidité en conséquence. Étant donné que les paramètres de _mintFee sont les anciennes valeurs de réserve, les frais sont calculés avec précision en se basant uniquement sur les changements de pool dus aux frais.
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokensS'il s'agit du premier dépôt, créer des jetons MINIMUM_LIQUIDITY et les envoyer à l'adresse zéro pour les verrouiller. Ils ne peuvent jamais être rachetés, ce qui signifie que le pool ne sera jamais complètement vidé (cela nous évite une division par zéro à certains endroits). La valeur de MINIMUM_LIQUIDITY est de mille, ce qui, si l'on considère que la plupart des ERC-20 sont subdivisés en unités de 10^-18 d'un jeton, comme l'ETH est divisé en wei, correspond à 10^-15 de la valeur d'un seul jeton. Le coût n'est pas élevé.
Au moment du premier dépôt, nous ne connaissons pas la valeur relative des deux jetons, nous multiplions donc simplement les montants et prenons une racine carrée, en supposant que le dépôt nous fournisse une valeur égale pour les deux jetons.
Nous pouvons nous y fier car il est dans l'intérêt du déposant de fournir une valeur égale pour éviter de perdre de la valeur à cause de l'arbitrage. Imaginons que la valeur des deux jetons est identique, mais que notre déposant ait déposé quatre fois plus de Jeton1 que de Jeton0. Un trader peut utiliser le fait que l'échange de paires pense que le Jeton0 a plus de valeur pour en extraire de la valeur.
| Événement | réserve0 | réserve1 | réserve0 * réserve1 | Valeur du pool (reserve0 + reserve1) |
|---|---|---|---|---|
| Configuration initiale | 8 | 32 | 256 | 40 |
| Le trader dépose 8 jetons Jeton0, récupère 16 Jeton1 | 16 | 16 | 256 | 32 |
Comme vous pouvez le voir, le trader a gagné 8 jetons supplémentaires, qui proviennent d'une réduction de la valeur du pool, ce qui pénalise le déposant qui le possède.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);À chaque dépôt ultérieur, nous connaissons déjà le taux de change entre les deux actifs et nous attendons des fournisseurs de liquidités qu'ils fournissent une valeur égale pour les deux. S'ils ne le font pas, nous leur donnons des jetons de liquidité basés sur la valeur inférieure qu'ils ont fournie, en guise de pénalité.
Qu'il s'agisse du dépôt initial ou d'un dépôt ultérieur, le nombre de jetons de liquidité que nous fournissons est égal à la racine carrée de la variation de reserve0*reserve1 et la valeur du jeton de liquidité ne change pas (sauf si nous recevons un dépôt qui n'a pas de valeurs égales pour les deux types, auquel cas l'« amende » est distribuée). Voici un autre exemple avec deux jetons qui ont la même valeur, avec trois bons dépôts et un mauvais (dépôt d'un seul type de jeton, il ne produit donc aucun jeton de liquidité).
| Événement | réserve0 | réserve1 | réserve0 * réserve1 | Valeur du pool (réserve0 + réserve1) | Jetons de liquidité frappés pour ce dépôt | Total des jetons de liquidité | valeur de chaque jeton de liquidité |
|---|---|---|---|---|---|---|---|
| Configuration initiale | 8,000 | 8,000 | 64 | 16,000 | 8 | 8 | 2,000 |
| Dépôt de quatre de chaque type | 12,000 | 12,000 | 144 | 24,000 | 4 | 12 | 2,000 |
| Dépôt de deux de chaque type | 14,000 | 14,000 | 196 | 28,000 | 2 | 14 | 2,000 |
| Dépôt de valeur inégale | 18,000 | 14,000 | 252 | 32,000 | 0 | 14 | ~2,286 |
| Après arbitrage | ~15,874 | ~15,874 | 252 | ~31,748 | 0 | 14 | ~2,267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);Utiliser la fonction UniswapV2ERC20._mint pour créer les jetons de liquidité supplémentaires et les donner au bon compte.
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 }Mettre à jour les variables d'état (reserve0, reserve1 et si nécessaire kLast) et émettre l'événement approprié.
brûler
1 // this low-level function should be called from a contract which performs important safety checks2 function burn(address to) external lock returns (uint amount0, uint amount1) {Cette fonction est appelée lorsque la liquidité est retirée et que les jetons de liquidité appropriés doivent être brûlés. Elle devrait également être appelée depuis un compte périphérique.
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];Le contrat périphérique a transféré la liquidité à brûler vers ce contrat avant l'appel. De cette façon, nous savons combien de liquidités brûler et nous pouvons nous assurer qu'elles sont bien brûlées.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');Le fournisseur de liquidités reçoit la même valeur pour les deux jetons. De cette façon, nous ne modifions pas le taux de change.
1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }11Afficher toutLe reste de la fonction burn est l'image miroir de la fonction mint ci-dessus.
échange
1 // this low-level function should be called from a contract which performs important safety checks2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {Cette fonction est également censée être appelée depuis un contrat périphérique.
1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errorsLes variables locales peuvent être stockées soit en mémoire, soit, si elles ne sont pas trop nombreuses, directement sur la pile. Si nous pouvons limiter le nombre, nous utiliserons la pile pour consommer moins de gaz. Pour plus de détails, voir le Yellow Paper, les spécifications formelles d'Ethereum (opens in a new tab), p. 26, équation 298.
1 address _token0 = token0;2 address _token1 = token1;3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokensCe transfert est optimiste, car nous transférons avant d'être sûrs que toutes les conditions sont remplies. Ceci est acceptable dans Ethereum car si les conditions ne sont pas remplies plus tard lors de l'appel, nous annulons l'opération ainsi que les changements qu'elle a créés.
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);Informer le destinataire de l'échange si demandé.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }Obtenir les soldes actuels. Le contrat périphérique nous envoie les jetons avant de nous appeler pour l'échange. Il est ainsi facile pour le contrat de vérifier qu'il n'est pas trompé, une vérification qui doit avoir lieu dans le contrat de base (car nous pouvons être appelés par d'autres entités que notre contrat périphérique).
1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');Ceci est un contrôle de cohérence pour s'assurer que nous ne perdons rien lors de l'échange. En aucune circonstance un échange ne devrait réduire reserve0*reserve1. C'est également là que nous nous assurons qu'une redevance de 0,3 % est envoyée sur l'échange ; avant de vérifier la valeur de K, nous multiplions les deux soldes par 1 000 et soustrayons les montants multipliés par 3, ce qui signifie que 0,3 % (3/1000 = 0,003 = 0,3 %) est déduit du solde avant de comparer sa valeur K à la valeur K des réserves actuelles.
1 }23 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }Mettre à jour reserve0 et reserve1, et si nécessaire les accumulateurs de prix et l'horodatage, et émettre un événement.
Sync ou Skim
Il est possible que les soldes réels se désynchronisent des réserves que l'échange de paires pense avoir.
Il n'y a aucun moyen de retirer des jetons sans le consentement du contrat, mais pour les dépôts c'est une autre affaire. Un compte peut transférer des jetons à l'échange sans appeler ni mint ni swap.
Dans ce cas, il y a deux solutions :
sync, mettre à jour les réserves avec les soldes actuelsskim, retirer le montant supplémentaire. Notez que tout compte est autorisé à appelerskimcar nous ne savons pas qui a déposé les jetons. Cette information est émise dans un événement, mais les événements ne sont pas accessibles depuis la blockchain.
1 // force balances to match reserves2 function skim(address to) external lock {3 address _token0 = token0; // gas savings4 address _token1 = token1; // gas savings5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // force reserves to match balances12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}Afficher toutUniswapV2Factory.sol
Ce contrat (opens in a new tab) crée les échanges de paires.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;Ces variables d'état sont nécessaires pour implémenter les frais de protocole (voir le livre blanc (opens in a new tab), p. 5).
L'adresse feeTo accumule les jetons de liquidité pour les frais de protocole, et feeToSetter est l'adresse autorisée à changer feeTo en une adresse différente.
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;Ces variables gardent une trace des paires, des échanges entre deux types de jetons.
La première, getPair, est un mappage qui identifie un contrat d'échange de paires basé sur les deux jetons ERC-20 qu'il échange. Les jetons ERC-20 sont identifiés par les adresses des contrats qui les implémentent, de sorte que les clés et la valeur sont toutes des adresses. Pour obtenir l'adresse de l'échange de paires qui vous permet de convertir tokenA en tokenB, vous utilisez getPair[<adresse de tokenA>][<adresse de tokenB>] (ou l'inverse).
La seconde variable, allPairs, est un tableau qui inclut toutes les adresses des échanges de paires créés par cette usine. Sur Ethereum, vous ne pouvez pas itérer sur le contenu d'un mapping, ni obtenir une liste de toutes les clés. Cette variable est donc le seul moyen de savoir quels échanges cette usine gère.
Note : La raison pour laquelle vous ne pouvez pas itérer sur toutes les clés d'un mapping est que le stockage des données de contrat est coûteux, donc moins nous en utilisons, et moins nous le modifions, mieux c'est. Vous pouvez créer des mappings qui supportent l'itération (opens in a new tab), mais ils nécessitent un stockage supplémentaire pour une liste de clés. Dans la plupart des applications, vous n'en avez pas besoin.
1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);Cet événement est émis lorsqu'un nouvel échange de paires est créé. Il comprend les adresses des jetons, l'adresse de l'échange de paires et le nombre total d'échanges gérés par l'usine.
1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 }La seule chose que fait le constructeur est de spécifier le feeToSetter. Les usines commencent sans frais, et seul feeSetter peut changer cela.
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }Cette fonction retourne le nombre de paires d'échange.
1 function createPair(address tokenA, address tokenB) external returns (address pair) {C'est la fonction principale de l'usine : créer un échange de paires entre deux jetons ERC-20. Notez que n'importe qui peut appeler cette fonction. Vous n'avez pas besoin d'autorisation d'Uniswap pour créer un nouvel échange de paires.
1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);Nous voulons que l'adresse du nouvel échange soit déterministe, afin qu'elle puisse être calculée à l'avance hors chaîne (cela peut être utile pour les transactions de couche 2). Pour ce faire, nous devons avoir un ordre cohérent des adresses de jetons, quel que soit l'ordre dans lequel nous les avons reçues. Nous les trions donc ici.
1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficientLes grands pools de liquidités sont préférables aux petits, car ils ont des prix plus stables. Nous ne voulons pas avoir plus d'un pool de liquidités par paire de jetons. S'il existe déjà un échange, il n'est pas nécessaire d'en créer un autre pour la même paire.
1 bytes memory bytecode = type(UniswapV2Pair).creationCode;Pour créer un nouveau contrat, nous avons besoin du code qui le crée (à la fois la fonction constructeur et le code qui écrit en mémoire le bytecode EVM du contrat réel). Normalement, dans Solidity, nous utilisons simplement addr = new <nom du contrat>(<paramètres du constructeur>) et le compilateur s'occupe de tout pour nous, mais pour avoir une adresse de contrat déterministe, nous devons utiliser l'opcode CREATE2 (opens in a new tab).
Lorsque ce code a été écrit, cet opcode n'était pas encore pris en charge par Solidity, il était donc nécessaire d'obtenir manuellement le code. Ce n'est plus un problème, car Solidity prend maintenant en charge CREATE2 (opens in a new tab).
1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));2 assembly {3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)4 }Lorsqu'un opcode n'est pas encore pris en charge par Solidity, nous pouvons l'appeler en utilisant l'assembly en ligne (opens in a new tab).
1 IUniswapV2Pair(pair).initialize(token0, token1);Appeler la fonction initialize pour indiquer au nouvel échange les deux jetons qu'il échange.
1 getPair[token0][token1] = pair;2 getPair[token1][token0] = pair; // populate mapping in the reverse direction3 allPairs.push(pair);4 emit PairCreated(token0, token1, pair, allPairs.length);Enregistrer les informations de la nouvelle paire dans les variables d'état et émettre un événement pour informer le monde du nouvel échange de paires.
1 function setFeeTo(address _feeTo) external {2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');3 feeTo = _feeTo;4 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}Afficher toutCes deux fonctions permettent à feeSetter de contrôler le destinataire des frais (le cas échéant) et de changer feeSetter en une nouvelle adresse.
UniswapV2ERC20.sol
Ce contrat (opens in a new tab) met en œuvre le jeton de liquidité ERC-20. Il est similaire au contrat ERC-20 d'OpenZeppelin, je n'expliquerai donc que la partie qui est différente, la fonctionnalité permit.
Les transactions sur Ethereum coûtent de l'ether (ETH), ce qui équivaut à de l'argent réel. Si vous avez des jetons ERC-20 mais pas d'ETH, vous ne pouvez pas envoyer de transactions, et vous ne pouvez donc rien en faire. Une solution pour éviter ce problème est d'utiliser les méta-transactions (opens in a new tab). Le propriétaire des jetons signe une transaction qui permet à une autre personne de retirer des jetons hors chaîne et l'envoie via Internet au destinataire. Le destinataire, qui possède des ETH, soumet alors le permis au nom du propriétaire.
1 bytes32 public DOMAIN_SEPARATOR;2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;Ce hachage est l'identifiant du type de transaction (opens in a new tab). Le seul que nous prenons en charge ici est Permit avec ces paramètres.
1 mapping(address => uint) public nonces;Il n'est pas possible pour un destinataire de falsifier une signature numérique. Cependant, il est trivial d'envoyer la même transaction deux fois (c'est une forme d'attaque par rejeu (opens in a new tab)). Pour éviter cela, nous utilisons un nonce (opens in a new tab). Si le nonce d'un nouveau Permit n'est pas supérieur de un au dernier nonce utilisé, nous le considérons comme invalide.
1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 }Ceci est le code pour récupérer l'identifiant de la chaîne (opens in a new tab). Il utilise un dialecte d'assembly EVM appelé Yul (opens in a new tab). Notez que dans la version actuelle de Yul, vous devez utiliser chainid(), et non chainid.
1 DOMAIN_SEPARATOR = keccak256(2 abi.encode(3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),4 keccak256(bytes(name)),5 keccak256(bytes('1')),6 chainId,7 address(this)8 )9 );10 }Afficher toutCalculer le séparateur de domaine (opens in a new tab) pour EIP-712.
1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {C'est la fonction qui implémente les permissions. Elle reçoit comme paramètres les champs pertinents, et les trois valeurs scalaires pour la signature (opens in a new tab) (v, r et s).
1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');Ne pas accepter les transactions après la date limite.
1 bytes32 digest = keccak256(2 abi.encodePacked(3 '\x19\x01',4 DOMAIN_SEPARATOR,5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))6 )7 );abi.encodePacked(...) est le message que nous nous attendons à recevoir. Nous savons ce que le nonce devrait être, il n'est donc pas nécessaire de l'obtenir en tant que paramètre.
L'algorithme de signature d'Ethereum attend 256 bits à signer, nous utilisons donc la fonction de hachage keccak256.
1 address recoveredAddress = ecrecover(digest, v, r, s);À partir du condensé et de la signature, nous pouvons obtenir l'adresse qui l'a signé en utilisant ecrecover (opens in a new tab).
1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }4Si tout est OK, traiter cela comme une approbation ERC-20 (opens in a new tab).
Les contrats périphériques
Les contrats périphériques sont l'API (interface de programme d'application) d'Uniswap. Ils sont disponibles pour les appels externes, en provenance d'autres contrats ou d'applications décentralisées. Vous pourriez appeler directement les contrats de base, mais c'est plus compliqué et vous pourriez perdre de la valeur si vous faites une erreur. Les contrats de base ne contiennent que des tests pour s'assurer qu'ils ne sont pas trompés, pas de contrôles de cohérence pour qui que ce soit d'autre. Ceux-ci sont dans la périphérie afin de pouvoir être mis à jour au besoin.
UniswapV2Router01.sol
Ce contrat (opens in a new tab) a des problèmes et ne devrait plus être utilisé (opens in a new tab). Heureusement, les contrats périphériques sont apatrides et ne détiennent aucun actif, il est donc facile de le déprécier et de suggérer aux gens d'utiliser le remplacement, UniswapV2Router02, à la place.
UniswapV2Router02.sol
Dans la plupart des cas, vous utiliseriez Uniswap via ce contrat (opens in a new tab). Vous pouvez voir comment l'utiliser ici (opens in a new tab).
1pragma solidity =0.6.6;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';Afficher toutLa plupart d'entre eux ont déjà été rencontrés ou sont assez évidents. La seule exception est IWETH.sol. Uniswap v2 permet les échanges pour n'importe quelle paire de jetons ERC-20, mais l'ether (ETH) lui-même n'est pas un jeton ERC-20. Il est antérieur à la norme et est transféré par des mécanismes uniques. Pour permettre l'utilisation d'ETH dans les contrats qui s'appliquent aux jetons ERC-20, le contrat wrapped ether (WETH) (opens in a new tab) a été créé. Vous envoyez des ETH à ce contrat, et il frappe un montant équivalent de WETH. Ou vous pouvez brûler du WETH, et récupérer de l'ETH.
1contract UniswapV2Router02 is IUniswapV2Router02 {2 using SafeMath for uint;34 address public immutable override factory;5 address public immutable override WETH;Le routeur a besoin de savoir quelle usine utiliser et, pour les transactions qui nécessitent des WETH, quel contrat WETH utiliser. Ces valeurs sont immuables (opens in a new tab), ce qui signifie qu'elles ne peuvent être définies que dans le constructeur. Cela donne aux utilisateurs l'assurance que personne ne pourra les modifier pour pointer vers des contrats moins honnêtes.
1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 }Ce modificateur garantit que les transactions limitées dans le temps (« faire X avant l'heure Y si possible ») ne se produisent pas après leur délai imparti.
1 constructor(address _factory, address _WETH) public {2 factory = _factory;3 WETH = _WETH;4 }Le constructeur définit simplement les variables d'état immuables.
1 receive() external payable {2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract3 }Cette fonction est appelée lorsque nous échangeons des jetons du contrat WETH contre des ETH. Seul le contrat WETH que nous utilisons est autorisé à faire cela.
Ajouter de la liquidité
Ces fonctions ajoutent des jetons à l'échange de paires, ce qui augmente le pool de liquidités.
12 // **** ADD LIQUIDITY ****3 function _addLiquidity(Cette fonction est utilisée pour calculer le montant des jetons A et B qui doivent être déposés dans l'échange de paires.
1 address tokenA,2 address tokenB,Ce sont les adresses des contrats de jetons ERC-20.
1 uint amountADesired,2 uint amountBDesired,Ce sont les montants que le fournisseur de liquidités souhaite déposer. Ce sont également les montants maximums de A et de B à déposer.
1 uint amountAMin,2 uint amountBMinCe sont les montants minimums acceptables pour le dépôt. Si la transaction ne peut pas avoir lieu avec ces montants ou plus, l'annuler. Si vous ne souhaitez pas cette fonctionnalité, spécifiez simplement zéro.
Les fournisseurs de liquidités spécifient un minimum, généralement parce qu'ils souhaitent limiter la transaction à un taux de change proche de celui en vigueur. Si le taux de change fluctue trop, cela peut signifier que des nouvelles ont modifié les valeurs sous-jacentes, et ils veulent décider manuellement de la marche à suivre.
Par exemple, imaginons un cas où le taux de change est de un pour un et où le fournisseur de liquidités spécifie ces valeurs :
| Paramètre | Valeur |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
Tant que le taux de change reste compris entre 0,9 et 1,25, la transaction a lieu. Si le taux de change sort de cette fourchette, la transaction est annulée.
Cette précaution s'explique par le fait que les transactions ne sont pas immédiates, vous les soumettez et un validateur finira par les inclure dans un bloc (à moins que le prix du gaz soit très bas, auquel cas vous devrez soumettre une autre transaction avec le même nonce et un prix du gaz plus élevé pour l'écraser). Vous ne pouvez pas contrôler ce qui se passe pendant l'intervalle entre la soumission et l'inclusion.
1 ) internal virtual returns (uint amountA, uint amountB) {La fonction retourne les montants que le fournisseur de liquidités doit déposer pour avoir un ratio égal au ratio actuel entre les réserves.
1 // create the pair if it doesn't exist yet2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);4 }S'il n'existe pas encore d'échange pour cette paire de jetons, le créer.
1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);Obtenir les réserves actuelles de la paire.
1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired);Si les réserves actuelles sont vides, alors il s'agit d'un nouvel échange de paires. Les montants à déposer doivent être exactement les mêmes que ceux que le fournisseur de liquidités souhaite fournir.
1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);Si nous devons voir quels seront les montants, nous obtenons le montant optimal en utilisant cette fonction (opens in a new tab). Nous voulons le même ratio que les réserves actuelles.
1 if (amountBOptimal <= amountBDesired) {2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 (amountA, amountB) = (amountADesired, amountBOptimal);Si amountBOptimal est inférieur au montant que le fournisseur de liquidités souhaite déposer, cela signifie que le jeton B a actuellement plus de valeur que ne le pense le déposant, un montant inférieur est donc requis.
1 } else {2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);3 assert(amountAOptimal <= amountADesired);4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');5 (amountA, amountB) = (amountAOptimal, amountBDesired);Si le montant optimal de B est supérieur au montant désiré de B, cela signifie que les jetons B ont actuellement moins de valeur que ne le pense le déposant de liquidités, un montant plus élevé est donc requis. Cependant, le montant souhaité est un maximum, nous ne pouvons donc pas le dépasser. Nous calculons donc le nombre optimal de jetons A pour le montant souhaité de jetons B.
En assemblant tout cela, nous obtenons ce graphique. Supposons que vous essayiez de déposer un millier de jetons A (ligne bleue) et un millier de jetons B (ligne rouge). L'axe des x représente le taux de change, A/B. Si x=1, ils ont la même valeur et vous déposez un millier de chaque. Si x=2, A vaut deux fois la valeur de B (vous obtenez deux jetons B pour chaque jeton A), vous déposez donc un millier de jetons B, mais seulement 500 jetons A. Si x=0,5, la situation est inversée : un millier de jetons A et cinq cents jetons B.
Vous pourriez déposer des liquidités directement dans le contrat de base (en utilisant UniswapV2Pair::mint (opens in a new tab)), mais le contrat de base vérifie uniquement qu'il ne se trompe pas lui-même. Vous risquez donc de perdre de la valeur si le taux de change évolue entre le moment où vous soumettez votre transaction et le moment où elle est exécutée. Si vous utilisez le contrat périphérique, il calcule le montant que vous devez déposer et le dépose immédiatement, de sorte que le taux de change ne change pas et vous ne perdez rien.
1 function addLiquidity(2 address tokenA,3 address tokenB,4 uint amountADesired,5 uint amountBDesired,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadlineAfficher toutCette fonction peut être appelée par une transaction pour déposer des liquidités. La plupart des paramètres sont les mêmes que dans _addLiquidity ci-dessus, à deux exceptions près :
. to est l'adresse qui reçoit les nouveaux jetons de liquidité frappés pour montrer la part du pool détenue par le fournisseur de liquidités.
deadline est une limite de temps pour la transaction.
1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);Nous calculons les montants à déposer réellement, puis nous trouvons l'adresse du pool de liquidités. Pour économiser du gaz, nous ne le faisons pas en interrogeant l'usine, mais en utilisant la fonction de bibliothèque pairFor (voir ci-dessous dans les bibliothèques).
1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);Transférer les bons montants de jetons de l'utilisateur vers l'échange de paires.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 }En retour, donner à l'adresse to des jetons de liquidité pour une propriété partielle du pool. La fonction mint du contrat de base voit combien de jetons supplémentaires elle a (par rapport à ce qu'elle avait la dernière fois que la liquidité a changé) et frappe la liquidité en conséquence.
1 function addLiquidityETH(2 address token,3 uint amountTokenDesired,Lorsqu'un fournisseur de liquidités souhaite fournir de la liquidité à un échange de paires Jeton/ETH, il y a quelques différences. Le contrat gère l'enveloppement de l'ETH pour le fournisseur de liquidités. Il n'est pas nécessaire de spécifier combien d'ETH l'utilisateur souhaite déposer, car il les envoie simplement avec la transaction (le montant est disponible dans msg.value).
1 uint amountTokenMin,2 uint amountETHMin,3 address to,4 uint deadline5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {6 (amountToken, amountETH) = _addLiquidity(7 token,8 WETH,9 amountTokenDesired,10 msg.value,11 amountTokenMin,12 amountETHMin13 );14 address pair = UniswapV2Library.pairFor(factory, token, WETH);15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);16 IWETH(WETH).deposit{value: amountETH}();17 assert(IWETH(WETH).transfer(pair, amountETH));Afficher toutPour déposer l'ETH, le contrat l'enveloppe d'abord dans du WETH, puis transfère le WETH dans la paire. Notez que le transfert est enveloppé dans un assert. Cela signifie que si le transfert échoue, cet appel de contrat échoue également, et donc l'enveloppement ne se produit pas réellement.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);4 }L'utilisateur nous a déjà envoyé les ETH. S'il reste un surplus (parce que l'autre jeton a moins de valeur que ne le pensait l'utilisateur), nous devons émettre un remboursement.
Retirer de la liquidité
Ces fonctions supprimeront la liquidité et rembourseront le fournisseur de liquidités.
1 // **** REMOVE LIQUIDITY ****2 function removeLiquidity(3 address tokenA,4 address tokenB,5 uint liquidity,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {Afficher toutLe cas le plus simple de retrait de liquidité. Il y a un montant minimum pour chaque jeton que le fournisseur de liquidités accepte de recevoir, et cela doit avoir lieu avant la date limite.
1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);La fonction burn du contrat de base gère le remboursement des jetons à l'utilisateur.
1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);Lorsqu'une fonction retourne plusieurs valeurs, mais que nous ne sommes intéressés que par certaines d'entre elles, voici comment nous obtenons uniquement ces valeurs. C'est un peu moins cher en termes de gaz que de lire une valeur et de ne jamais l'utiliser.
1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);Traduire les montants de la façon dont le contrat de base les renvoie (jeton d'adresse inférieure en premier) à la manière dont l'utilisateur les attend (correspondant à tokenA et tokenB).
1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 }Il est acceptable d'effectuer le transfert d'abord, puis de vérifier sa légitimité, car si ce n'est pas le cas, nous annulerons tous les changements d'état.
1 function removeLiquidityETH(2 address token,3 uint liquidity,4 uint amountTokenMin,5 uint amountETHMin,6 address to,7 uint deadline8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {9 (amountToken, amountETH) = removeLiquidity(10 token,11 WETH,12 liquidity,13 amountTokenMin,14 amountETHMin,15 address(this),16 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }Afficher toutLe retrait de liquidité pour l'ETH est presque identique, sauf que nous recevons les jetons WETH, puis les échangeons contre de l'ETH pour les restituer au fournisseur de liquidités.
1 function removeLiquidityWithPermit(2 address tokenA,3 address tokenB,4 uint liquidity,5 uint amountAMin,6 uint amountBMin,7 address to,8 uint deadline,9 bool approveMax, uint8 v, bytes32 r, bytes32 s10 ) external virtual override returns (uint amountA, uint amountB) {11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);15 }161718 function removeLiquidityETHWithPermit(19 address token,20 uint liquidity,21 uint amountTokenMin,22 uint amountETHMin,23 address to,24 uint deadline,25 bool approveMax, uint8 v, bytes32 r, bytes32 s26 ) external virtual override returns (uint amountToken, uint amountETH) {27 address pair = UniswapV2Library.pairFor(factory, token, WETH);28 uint value = approveMax ? uint(-1) : liquidity;29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);31 }Afficher toutCes fonctions relaient les méta-transactions pour permettre aux utilisateurs sans ether de se retirer du pool, en utilisant le mécanisme de permis.
1 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****2 function removeLiquidityETHSupportingFeeOnTransferTokens(3 address token,4 uint liquidity,5 uint amountTokenMin,6 uint amountETHMin,7 address to,8 uint deadline9 ) public virtual override ensure(deadline) returns (uint amountETH) {10 (, amountETH) = removeLiquidity(11 token,12 WETH,13 liquidity,14 amountTokenMin,15 amountETHMin,16 address(this),17 deadline18 );19 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));20 IWETH(WETH).withdraw(amountETH);21 TransferHelper.safeTransferETH(to, amountETH);22 }23Afficher toutCette fonction peut être utilisée pour les jetons qui ont des frais de transfert ou de stockage. Lorsqu'un jeton a de tels frais, nous ne pouvons pas compter sur la fonction removeLiquidity pour nous dire combien de jetons nous récupérons. Nous devons donc d'abord retirer, puis obtenir le solde.
123 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline,10 bool approveMax, uint8 v, bytes32 r, bytes32 s11 ) external virtual override returns (uint amountETH) {12 address pair = UniswapV2Library.pairFor(factory, token, WETH);13 uint value = approveMax ? uint(-1) : liquidity;14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(16 token, liquidity, amountTokenMin, amountETHMin, to, deadline17 );18 }Afficher toutLa fonction finale combine les frais de stockage avec les méta-transactions.
Commercer
1 // **** SWAP ****2 // nécessite que le montant initial ait déjà été envoyé à la première paire3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {Cette fonction réalise un traitement interne requis pour les fonctions qui sont exposées aux traders.
1 for (uint i; i < path.length - 1; i++) {Au moment où j'écris ces lignes, il existe 388 160 jetons ERC-20 (opens in a new tab). S'il y avait un échange de paires pour chaque paire de jetons, cela représenterait plus de 150 milliards d'échanges de paires. La chaîne entière, à l'heure actuelle, ne compte que 0,1 % de ce nombre de comptes (opens in a new tab). Les fonctions d'échange supportent plutôt le concept de chemin. Un trader peut échanger A contre B, B contre C et C contre D. Ainsi, il n'y a pas besoin d'un échange direct de paire A-D.
Les prix sur ces marchés tendent à être synchronisés, car quand ils sont désynchronisés, cela crée une opportunité d'arbitrage. Imaginez, par exemple, trois jetons : A, B et C. Il existe donc trois échanges de paires, un pour chaque paire.
- La situation initiale
- Un trader vend 24,695 jetons A et obtient 25,305 jetons B.
- Le trader vend 24,695 jetons B contre 25,305 jetons C, et garde environ 0,61 jeton B comme bénéfice.
- Puis le trader vend 24,695 jetons C en échange de 25,305 jetons A, et garde environ 0,61 jeton C comme bénéfice. Le trader a également 0,61 jeton A supplémentaire (les 25,305 dont dispose le trader en fin de transaction moins l'investissement initial de 24,695).
| Étape | Échange A-B | Échange B-C | Échange A-C |
|---|---|---|---|
| 1 | A : 1 000 B : 1 050 A/B=1,05 | B : 1 000 C : 1 050 B/C=1,05 | A : 1 050 C : 1 000 C/A=1,05 |
| 2 | A : 1024,695 B : 1024,695 A/B=1 | B : 1 000 C : 1 050 B/C=1,05 | A : 1 050 C : 1 000 C/A=1,05 |
| 3 | A : 1024,695 B : 1024,695 A/B=1 | B : 1024,695 C : 1024,695 B/C=1 | A : 1 050 C : 1 000 C/A=1,05 |
| 4 | A : 1024,695 B : 1024,695 A/B=1 | B : 1024,695 C : 1024,695 B/C=1 | A : 1024,695 C : 1024,695 C/A=1 |
1 (address input, address output) = (path[i], path[i + 1]);2 (address token0,) = UniswapV2Library.sortTokens(input, output);3 uint amountOut = amounts[i + 1];Obtenez la paire que nous traitons actuellement, triez-la (pour l'utiliser avec la paire) et obtenez le montant de sortie attendu.
1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));Obtenez les montants de sortie attendus, triés de la manière attendue par l'échange de paires.
1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;Est-ce le dernier échange ? Si c'est le cas, envoyez les jetons reçus pour la transaction à sa destination. Si ce n'est pas le cas, envoyez-le à la prochaine paire d'échange.
12 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(3 amount0Out, amount1Out, to, new bytes(0)4 );5 }6 }Appelez réellement l'échange de paires pour échanger les jetons. Nous n'avons pas besoin d'un rappel pour être informé de l'échange. De fait, nous n'envoyons pas d'octets dans ce champ.
1 function swapExactTokensForTokens(Cette fonction est utilisée directement par les traders pour échanger un jeton contre un autre.
1 uint amountIn,2 uint amountOutMin,3 address[] calldata path,Ce paramètre contient les adresses des contrats ERC-20. Comme expliqué ci-dessus, il s'agit d'un tableau parce que vous pourriez avoir besoin de passer par plusieurs échanges de paires pour passer de l'actif que vous avez à l'actif que vous voulez.
Un paramètre de fonction dans Solidity peut être stocké soit en memory, soit en calldata. Si la fonction est un point d'entrée du contrat, directement appelée par un utilisateur (en utilisant une transaction) ou à partir d'un contrat différent, la valeur du paramètre peut être directement obtenue à partir des données d'appel. Si la fonction est appelée en interne, comme _swap ci-dessus, alors les paramètres doivent être stockés en memory. Du point de vue du contrat appelé, calldata est en lecture seule.
Avec des types scalaires tels que uint ou address, le compilateur gère le choix de stockage pour nous, mais avec les tableaux, qui sont plus longs et plus coûteux, nous spécifions le type de stockage à utiliser.
1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {Les valeurs de retour sont toujours retournées en mémoire.
1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');Calculez le montant à acheter pour chaque échange. Si le résultat est inférieur au minimum que le trader est prêt à accepter, annulez la transaction.
1 TransferHelper.safeTransferFrom(2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]3 );4 _swap(amounts, path, to);5 }Enfin, transférez le jeton ERC-20 initial sur le compte pour l'échange de la première paire et appelez _swap. Tout cela se passe dans la même transaction. Ainsi, l'échange de paires sait que tous les jetons inattendus font partie de ce transfert.
1 function swapTokensForExactTokens(2 uint amountOut,3 uint amountInMax,4 address[] calldata path,5 address to,6 uint deadline7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');10 TransferHelper.safeTransferFrom(11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]12 );13 _swap(amounts, path, to);14 }Afficher toutLa fonction précédente, swapTokensForTokens, permet à un trader de spécifier le nombre exact de jetons d'entrée qu'il est prêt à donner et le nombre minimum de jetons de sortie qu'il est prêt à recevoir en retour. Cette fonction fait l'échange inverse, elle permet à un trader de spécifier le nombre de jetons de sortie qu'il veut, ainsi que le nombre maximum de jetons d'entrée qu'il est prêt à payer.
Dans les deux cas, le trader doit d'abord donner à ce contrat périphérique une autorisation pour lui permettre de les transférer.
1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)2 external3 virtual4 override5 payable6 ensure(deadline)7 returns (uint[] memory amounts)8 {9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');12 IWETH(WETH).deposit{value: amounts[0]}();13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));14 _swap(amounts, path, to);15 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 ensure(deadline)23 returns (uint[] memory amounts)24 {25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');28 TransferHelper.safeTransferFrom(29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]30 );31 _swap(amounts, path, address(this));32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);34 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 ensure(deadline)43 returns (uint[] memory amounts)44 {45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');48 TransferHelper.safeTransferFrom(49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]50 );51 _swap(amounts, path, address(this));52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);54 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 ensure(deadline)63 returns (uint[] memory amounts)64 {65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');68 IWETH(WETH).deposit{value: amounts[0]}();69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));70 _swap(amounts, path, to);71 // refund dust eth, if any72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }Afficher toutCes quatre variantes impliquent toutes des échanges entre ETH et des jetons. La seule différence est que nous recevons de l'ETH du trader et que nous l'utilisons pour frapper des WETH, ou nous recevons des WETH du dernier échange et les brûlons, renvoyant au trader l'ETH en résultant.
1 // **** SWAP (supporting fee-on-transfer tokens) ****2 // requires the initial amount to have already been sent to the first pair3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {C'est la fonction interne pour échanger les jetons qui ont des frais de transfert ou de stockage pour résoudre (ce problème (opens in a new tab)).
1 for (uint i; i < path.length - 1; i++) {2 (address input, address output) = (path[i], path[i + 1]);3 (address token0,) = UniswapV2Library.sortTokens(input, output);4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));5 uint amountInput;6 uint amountOutput;7 { // portée pour éviter les erreurs de pile trop profonde8 (uint reserve0, uint reserve1,) = pair.getReserves();9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);Afficher toutEn raison des frais de transfert, nous ne pouvons pas nous fier à la fonction getAmountsOut pour nous dire combien nous obtenons de chaque transfert (comme nous le faisons avant d'appeler le _swap original). Au lieu de cela, nous devons d'abord transférer et ensuite voir combien de jetons nous avons récupérés.
Remarque : En théorie, nous pourrions simplement utiliser cette fonction au lieu de _swap, mais dans certains cas (par exemple, si le transfert est annulé parce qu'il n'y a pas assez à la fin pour atteindre le minimum requis), cela finirait par coûter plus de gaz. Les jetons avec frais de transfert sont assez rares. Bien que nous devions les prendre en charge, il n'est pas nécessaire que tous les échanges partent du principe qu'ils en impliquent au moins un.
1 }2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;4 pair.swap(amount0Out, amount1Out, to, new bytes(0));5 }6 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);20 _swapSupportingFeeOnTransferTokens(path, to);21 require(22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'24 );25 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 ensure(deadline)39 {40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');41 uint amountIn = msg.value;42 IWETH(WETH).deposit{value: amountIn}();43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);45 _swapSupportingFeeOnTransferTokens(path, to);46 require(47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'49 );50 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 ensure(deadline)64 {65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');66 TransferHelper.safeTransferFrom(67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn68 );69 _swapSupportingFeeOnTransferTokens(path, address(this));70 uint amountOut = IERC20(WETH).balanceOf(address(this));71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');72 IWETH(WETH).withdraw(amountOut);73 TransferHelper.safeTransferETH(to, amountOut);74 }Afficher toutCe sont les mêmes variantes utilisées pour les jetons normaux, mais elles appellent _swapSupportingFeeOnTransferTokens à la place.
1 // **** FONCTIONS DE LA BIBLIOTHÈQUE ****2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {3 return UniswapV2Library.quote(amountA, reserveA, reserveB);4 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}Afficher toutCes fonctions sont simplement des proxys qui appellent les fonctions d'UniswapV2Library.
UniswapV2Migrator.sol
Ce contrat a été utilisé pour migrer les échanges de l'ancienne v1 vers la v2. Maintenant qu'ils ont été migrés, il n'est plus pertinent.
Les bibliothèques
La bibliothèque SafeMath (opens in a new tab) est bien documentée, il n'est donc pas nécessaire de la documenter ici.
Math
Cette bibliothèque contient des fonctions mathématiques qui ne sont pas habituellement nécessaires dans le code Solidity. Elles ne font donc pas partie du langage.
1pragma solidity =0.5.16;23// une bibliothèque pour effectuer diverses opérations mathématiques45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // méthode babylonienne (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)11 function sqrt(uint y) internal pure returns (uint z) {12 if (y > 3) {13 z = y;14 uint x = y / 2 + 1;Afficher toutCommencez avec x comme une estimation supérieure à la racine carrée (c'est la raison pour laquelle nous devons traiter 1-3 comme des cas particuliers).
1 while (x < z) {2 z = x;3 x = (y / x + x) / 2;Obtenez une estimation plus précise, la moyenne de l'estimation précédente et le nombre dont nous essayons de trouver la racine carrée divisée par l'estimation précédente. Répétez jusqu'à ce que la nouvelle estimation ne soit pas inférieure à celle existante. Pour plus de détails, voir ici (opens in a new tab).
1 }2 } else if (y != 0) {3 z = 1;Nous ne devrions jamais avoir besoin de la racine carrée de zéro. Les racines carrées de un, deux et trois sont approximativement un (nous utilisons des entiers, donc nous ignorons la fraction).
1 }2 }3}Fractions à point fixe (UQ112x112)
Cette bibliothèque gère les fractions qui ne font normalement pas partie de l'arithmétique Ethereum. Elle le fait en encodant le nombre x comme x*2^112. Cela nous permet d'utiliser les codes d'opérations d'addition et de soustraction d'origine sans modification.
1pragma solidity =0.5.16;23// une bibliothèque pour la gestion des nombres à virgule fixe binaires (https://wikipedia.org/wiki/Q_(number_format))45// plage : [0, 2**112 - 1]6// résolution : 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;Afficher toutQ112 est l'encodage pour un.
1 // encode un uint112 en UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // ne provoque jamais de dépassement4 }Puisque y est un uint112, sa valeur maximale est 2^112-1. Ce nombre peut toujours être encodé en UQ112x112.
1 // divise un UQ112x112 par un uint112, retournant un UQ112x1122 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {3 z = x / uint224(y);4 }5}Si nous divisons deux valeurs UQ112x112, le résultat n'est plus multiplié par 2^112. Ainsi, nous prenons à la place un entier comme dénominateur. Nous aurions dû utiliser une astuce similaire pour faire la multiplication, mais nous n'avons pas besoin de faire la multiplication de la valeur UQ112x112.
UniswapV2Library
Cette bibliothèque n'est utilisée que par les contrats périphériques
1pragma solidity >=0.5.0;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // renvoie les adresses des jetons triés, utilisé pour gérer les valeurs de retour des paires triées dans cet ordre11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');15 }Afficher toutTriez les deux jetons par adresse, afin que nous puissions obtenir l'adresse de l'échange de paires pour eux. Ceci est nécessaire car sinon nous aurions deux possibilités, une pour les paramètres A, B et une autre pour les paramètres B, A, ce qui conduirait à deux échanges au lieu d'un seul.
1 // calcule l'adresse CREATE2 pour une paire sans effectuer d'appels externes2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {3 (address token0, address token1) = sortTokens(tokenA, tokenB);4 pair = address(uint(keccak256(abi.encodePacked(5 hex'ff',6 factory,7 keccak256(abi.encodePacked(token0, token1)),8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // hachage du code d'initialisation9 ))));10 }Afficher toutCette fonction calcule l'adresse de l'échange en paire pour les deux jetons. Ce contrat est créé en utilisant le code d'opération CREATE2 (opens in a new tab). Nous pouvons donc calculer l'adresse en utilisant le même algorithme si nous connaissons les paramètres qu'il utilise. C'est beaucoup moins cher que de demander à la factory, et
1 // récupère et trie les réserves pour une paire2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {3 (address token0,) = sortTokens(tokenA, tokenB);4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);6 }Cette fonction retourne les réserves des deux jetons que l'échange en paire possède. Notez qu'il peut recevoir les jetons dans n'importe quel ordre, et les trier pour un usage interne.
1 // étant donné un certain montant d'un actif et des réserves de paires, renvoie un montant équivalent de l'autre actif2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 amountB = amountA.mul(reserveB) / reserveA;6 }Cette fonction vous donne le montant du jeton B que vous obtiendrez en échange du jeton A s'il n'y a pas de frais engagés. Ce calcul prend en compte le fait que le transfert modifie le taux de change.
1 // étant donné un montant d'entrée d'un actif et des réserves de paires, renvoie le montant de sortie maximum de l'autre actif2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {La fonction quote ci-dessus fonctionne bien s'il n'y a pas de frais pour l'utilisation de l'échange en paire. Cependant, s'il existe des frais de change de 0,3 %, le montant que vous obtenez réellement est inférieur. Cette fonction calcule le montant après les frais de change.
12 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 }Solidity ne gère pas nativement les fractions. Ainsi, nous ne pouvons pas simplement multiplier le montant par 0,997. Au lieu de cela, nous multiplions le numérateur par 997 et le dénominateur par 1 000, ce qui a le même effet.
1 // étant donné un montant de sortie d'un actif et des réserves de paires, renvoie le montant d'entrée requis de l'autre actif2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 uint numerator = reserveIn.mul(amountOut).mul(1000);6 uint denominator = reserveOut.sub(amountOut).mul(997);7 amountIn = (numerator / denominator).add(1);8 }Cette fonction réalise à peu près la même chose, mais elle récupère le montant en sortie et fournit l'entrée.
12 // effectue des calculs de getAmountOut en chaîne sur un nombre quelconque de paires3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }1213 // effectue des calculs de getAmountIn en chaîne sur un nombre quelconque de paires14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');16 amounts = new uint[](path.length);17 amounts[amounts.length - 1] = amountOut;18 for (uint i = path.length - 1; i > 0; i--) {19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);21 }22 }23}Afficher toutCes deux fonctions permettent d'identifier les valeurs lorsqu'il est nécessaire de passer par plusieurs échanges de paires.
Aide au transfert
Cette bibliothèque (opens in a new tab) ajoute des contrôles de réussite autour des transferts ERC-20 et Ethereum pour traiter de la même manière une annulation et un retour de valeur false.
1// SPDX-License-Identifier: GPL-3.0-or-later23pragma solidity >=0.6.0;45// méthodes d'aide pour interagir avec les jetons ERC20 et envoyer des ETH qui ne renvoient pas systématiquement true/false6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14Afficher toutNous pouvons appeler un autre contrat de deux manières :
- Utiliser une définition d'interface pour créer un appel de fonction
- Utiliser l'interface binaire de l'application (ABI) (opens in a new tab) « manuellement » pour créer l'appel. C'est ce que l'auteur du code a décidé de faire.
1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 }Pour des raisons de rétrocompatibilité avec les jetons créés avant la norme ERC-20, un appel ERC-20 peut échouer soit en étant annulé (auquel cas success est false), soit en réussissant mais en retournant une valeur false (auquel cas il y a des données de sortie, et si vous les décodez en tant que booléen, vous obtenez false).
123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transfer(address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::safeTransfer: transfer failed'13 );14 }Afficher toutCette fonction implémente la fonctionnalité de transfert d'ERC-20 (opens in a new tab), qui permet à un compte de dépenser la provision fournie par un autre compte.
12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::transferFrom: transferFrom failed'13 );14 }Afficher toutCette fonction implémente la fonctionnalité transferFrom d'ERC-20 (opens in a new tab), qui permet à un compte de dépenser la provision fournie par un autre compte.
12 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6}Cette fonction transfère de l'éther vers un compte. Tout appel à un autre contrat peut tenter d'envoyer de l'éther. Parce que nous n'avons pas besoin d'appeler une quelconque fonction, nous n'envoyons aucune donnée avec l'appel.
Conclusion
Ceci est un long article d'environ 50 pages. Si vous êtes arrivé jusqu'ici, félicitations ! Espérons que maintenant vous avez compris les considérations à prendre en compte pour écrire une application réelle (par opposition aux programmes d'exemple courts) et que vous êtes mieux à même d'écrire des contrats pour vos propres cas d'utilisation.
Et maintenant, à vous d'écrire quelque chose d'utile et de nous étonner.
Voir ici pour plus de mon travail (opens in a new tab).
Dernière mise à jour de la page : 25 février 2026
