Passer au contenu principal

Visite guidée du contrat Uniswap-v2

solidity
Intermédiaire
Ori Pomerantz
1 mai 2021
63 minutes de lecture minute read

Introduction

Uniswap v2(opens in a new tab) permet de 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 négociants également appelés traders.

Les fournisseurs de liquidités fournissent la réserve (« 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, appelé un jeton de liquidité, qui représente la détention partielle du pool.

Les traders envoient un type de jeton au pool et reçoivent l'autre (par exemple, envoie le Jeton0 et reçoit le Jeton1) du pool, mis à disposition 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 récupère 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 récupérer 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 à jour beaucoup plus complexe que la v2. Il est plus facile de découvrir d'abord la v2, puis de passer à la v3.

Contrats de base vs 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 à vérifier. 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

Ce sont les flux de données et de contrôle qui se produisent lorsque vous effectuez les trois actions principales d'Uniswap :

  1. Échanger différents jetons
  2. Ajouter des liquidités sur le marché et obtenir des récompenses sous forme de jetons de liquidité ERC
  3. Brûler des jetons de liquidité ERC-20 et récupérer des jetons ERC-20 que l'échange en paire permet aux traders d'échanger

Échanger

C'est le flux le plus fréquemment utilisé par les traders :

Appelant

  1. Fournir au compte périphérique une provision correspondant au montant à échanger.
  2. Appeler l'une des nombreuses fonctions d'échange du contrat périphérique (celui dont on dépend si ETH est impliqué ou non, si le trader spécifie le nombre de jetons à déposer ou le nombre de jetons à récupérer, etc). Chaque fonction d'échange accepte un chemin path, un tableau d'échanges à parcourir.

Dans le contrat périphérique (UniswapV2Router02.sol)

  1. Identifier les montants qui doivent être négociés à chaque échange tout au long du chemin.
  2. Itèrer sur le chemin. Pour chaque échange réalisé en cours de route, il envoie le jeton d'entrée puis appelle la fonction swap pour l'échange. Dans la plupart des cas, l'adresse de destination des jetons est la prochaine paire d'échange sur le chemin. Dans l'échange final, c'est l'adresse fournie par le trader.

Dans le contrat de base (UniswapV2Pair.sol)

  1. Vérifier que le contrat de base n'est pas faussé et qu'il dispose de suffisamment de liquidités après l'échange.
  2. Constater combien de jetons supplémentaires nous avons en plus des réserves connues. Ce montant est le nombre de jetons d'entrée que nous avons reçus et à échanger.
  3. Envoyer les jetons de sortie à la destination.
  4. Appeler _update pour mettre à jour les montants de la réserve

Retour au contrat périphérique (UniswapV2Router02.sol)

  1. Effectuer tout nettoyage nécessaire (par exemple, brûler des jetons WETH pour récupérer des ETH à envoyer au trader)

Ajouter des liquidités

Appelant

  1. Fournir au compte périphérique une provision à ajouter à la réserve de liquidités.
  2. Appeler l'une des fonctions addLiquidity du contrat périphérique.

Dans le contrat périphérique (UniswapV2Router02.sol)

  1. Créer un nouvel échange de paires si nécessaire
  2. S'il existe un échange de paires existant, calculer le nombre de jetons à ajouter. La valeur est sensée être identique pour les deux jetons, soit le même ratio entre les nouveaux jetons et les jetons existants.
  3. 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é)
  4. Appeler le contrat de base.

Dans le contrat de base (UniswapV2Pair.sol)

  1. Frapper des jetons de liquidité et les envoyer à l'appelant
  2. Appeler _update pour mettre à jour les montants de la réserve

Retirer la liquidité

Appelant

  1. Fournir au compte périphérique une provision de jetons de liquidité à brûler en échange des jetons sous-jacents.
  2. Appeler l'une des fonctions removeLiquidity du contrat périphérique.

Dans le contrat périphérique (UniswapV2Router02.sol)

  1. Envoyer les jetons de liquidité à l'échange de paire

Dans le contrat de base (UniswapV2Pair.sol)

  1. Envoie l'adresse de destination des 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, 90 jetons de liquidité, et que nous recevons 9 jetons à brûler, nous brûlons 10 % des jetons de liquidité et nous renvoyons donc à l'utilisateur 100 jetons A et 50 jetons B.
  2. Brûler les jetons de liquidité
  3. Appeler _update pour mettre à jour les montants de la réserve

Les contrats de base

Ce sont les contrats sécurisés qui détiennent les liquidités.

UniswapV2Pair.sol

Ce contrat(opens in a new tab) implémente le pool d'échange effectif des jetons. C'est une fonctionnalité de base d'Uniswap.

1pragma solidity =0.5.16;
2
3import './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 tout
Copier

Ce 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 {
Copier

Ce contrat hérite de UniswapV2ERC20, qui fournit les fonctions ERC-20 pour les jetons de liquidité.

1 using SafeMath for uint;
Copier

La bibliothèque SafeMath(opens in a new tab) est utilisée pour éviter les dépassements et soupassements. C'est important car sinon nous pourrions nous retrouver dans une situation où une valeur devrait être -1, mais est en réalité 2^256-1.

1 using UQ112x112 for uint224;
Copier

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.

D'autres informations sur cette bibliothèque sont disponibles plus bas dans ce document.

Variables

1 uint public constant MINIMUM_LIQUIDITY = 10**3;
Copier

Pour éviter les cas de division par zéro, il existe toujours un nombre minimum de jetons de liquidité (mais détenus par le compte zéro). Ce nombre est LIQUIDITÉ_MINIMUM, un millier.

1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
Copier

C'est le sélecteur ABI pour la fonction de transfert ERC-20. Il est utilisé pour transférer les jetons ERC-20 dans les deux comptes de jetons.

1 address public factory;
Copier

C'est le contrat d'usine qui a créé ce pool. Chaque pool est un échange entre deux jetons ERC-20, l'usine étant le point central qui relie tous ces pools.

1 address public token0;
2 address public token1;
Copier

Il y a 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 getReserves
2 uint112 private reserve1; // uses single storage slot, accessible via getReserves
Copier

La réserve dont dispose le pool pour chaque type de jeton. Nous supposons que les deux ensembles représentent le même montant de valeur, et ainsi que chaque jeton0 vaut le jeton1 reserve1/reserve0.

1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
Copier

L'horodatage du dernier bloc dans lequel un échange a eu lieu, utilisé pour suivre les taux de change auu fil du temps.

L'une des dépenses de gaz les plus importantes des contrats Ethereum est relative au 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;
Copier

Ces variables contiennent les coûts cumulatifs pour chaque jeton (chacune en fonction de l'autre). Ils peuvent être utilisés 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 event
Copier

Dans le cadre de l'échange de paires, le taux de change entre le jeton0 et jeton 1 est déterminé en gardant le multiple des deux réserves constant lors des échanges. kLast est cette valeur. Elle change lors du dépot de liquidités par un fournisseur ou des retraits de jetons et augmente légèrement à cause des frais de marché de 0,3 %.

Voici un exemple simple. Notez que pour des raisons de simplicité, le tableau n'affiche que trois chiffres après la virgule et que nous ignorons le taux de change de 0,3 % et qu'ainsi, les chiffres présentés sont inexacts.

Evénementreserve0reserve1reserve0 * reserve1Taux de change moyen (jeton1 / jeton0)
Configuration initiale1.000,0001.000,0001.000.000
Le trader A échange 50 jetons0 contre 47,619 jetons11.050,000952,3811.000.0000,952
Le trader B échange 10 jetons0 contre 8,984 jetons11.060,000943,3961.000.0000,898
Le trader C échange 40 jetons0 pcontre 34,305 jetons11.100,000909,0901.000.0000,858
Le trader D échange 100 jetons1 contre 109,01 jetons0990,9901.009,0901.000.0000,917
Le trader E échange 10 jetons0 contre 10,079 jetons11.000.990999.0101.000,0001,008

Comme les traders fournissent plus de jetons0, la valeur relative du jeton1 augmente et vice versa, en fonction de l'offre et de la demande.

Verrouiller

1 uint private unlocked = 1;
Copier

Il existe une classe de vulnérabilités de sécurité liées à la faille de réentrance(opens in a new tab). Uniswap doit transférer arbitrairement des jetons ERC-20, ce qui signifie appler des contrats ERC-20 qui pourraient tenter d'utiliser à mauvais escient le marché Uniswap. En disposant d'une variable unlocked comme partie du contrat, nous pouvons empêcher les fonctions d'être appelées pendant qu'elles sont en cours d'exécution (au cours de la même transaction).

1 modifier lock() {
Copier

Cette fonction est un modificateur(opens in a new tab), une fonction qui entoure une fonction normale pour changer son comportement d'une manière ou d'une autre.

1 require(unlocked == 1, 'UniswapV2: LOCKED');
2 unlocked = 0;
Copier

Si unlocked est égal à un, définissez-la à zéro. Si elle est déjà à zéro, faites échouer l'appel.

1 _;
Copier

Dans un modificateur _ ; est l'appel à la fonction originale (avec tous les paramètres). Ici, cela signifie que l'appel de fonction ne se produit que si unlocked était un paramètre appelé, et que, lors de son exécution, la valeur de unlocked est zéro.

1 unlocked = 1;
2 }
Copier

Après le retour de la fonction principale, déverrouillez.

Misc. fonctions

1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
2 _reserve0 = reserve0;
3 _reserve1 = reserve1;
4 _blockTimestampLast = blockTimestampLast;
5 }
Copier

Cette fonction fournit aux appelants l'état courant de l'échange. Notez que les fonctions Solidity peuvent faire remonter des valeurs multiples(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));
Copier

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 définition ci-dessus).

Pour éviter d'avoir à importer une interface pour la fonction 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 }
Copier

Un appel de transfert ERC-20 peut signaler un échec de deux façons :

  1. Rétablir. Lorsque l'appel à un contrat externe indique que la valeur de retour booléen est false
  2. Terminer normalement mais signaler un échec. Dans ce cas, le tampon de valeur de retour a une longueur non nulle et, lorsque décodé comme une valeur booléenne, est false

Si l'une ou l'autre de ces conditions se produit, revenez en arrière.

É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);
Copier

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, comme également l'identité du compte qui les 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 to
8 );
Copier

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);
Copier

Enfin, Sync est émis chaque fois que des jetons sont ajoutés ou retirés, quelle que 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 de la nouvelle paire d’échange.

1 constructor() public {
2 factory = msg.sender;
3 }
Copier

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 initialiser et pour les frais d'usine (s'il y en a)

1 // called once by the factory at time of deployment
2 function initialize(address _token0, address _token1) external {
3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
4 token0 = _token0;
5 token1 = _token1;
6 }
Copier

Cette fonction permet à l'usine (et seulement à l'usine) de spécifier les deux jetons ERC-20 que cette paire va échanger.

Fonctions de mise à jour interne

_update
1 // update reserves and, on the first call per block, price accumulators
2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
Copier

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');
Copier

Si solde0 ou solde1 (uint256) est supérieur à uint112(-1) (=2^112-1) (donc il déborde et se retrouve à 0 lorsqu'il est converti en uint112), refusez de continuer la _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 posé aucun problème.

1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);
2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
Copier

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 desired
2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
4 }
Copier

Chaque accumulateur de coûts est mis à jour en tenant compte du 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 prenez le prix cumulé de deux points dans le temps et le divisez par la différence de temps entre ces deux points. Par exemple, supposons cette séquence d'événements :

Evénementréserve0réserve1horodatageTaux de change marginal (réserve1 / réserve0)DernierprixCumulé0
Configuration initiale1.000,0001.000,0005.0001,0000
Le Trader A dépose 50 jetons0 et récupère 47,619 jetons11.050,000952,3815.0200,90720
Le Trader B dépose 10 jetons0 et récupère 8,984 jetons11.060,000943,3965.0300,89020+10*0,907 = 29,07
Le Trader C dépose 40 jetons0 et récupère 34,305 jetons11.100,000909,0905.1000,82629,07+70*0,890 = 91,37
Le Trader D dépose 100 jetons1 et récupère 109,01 jetons0990,9901.009,0905.1101,01891,37+10*0,826 = 99,63
Le Trader E dépose 10 jetons0 et récupère 10,079 jetons11.000,990999,0105.1500,99899,63+40*1,1018 = 143,702

Disons que nous souhaitons calculer le prix moyen du Jeton entre les horodatages 5.030 et 5.150. La différence de valeur du price0Cumulative est 143,702-29,07=114,632. Il s'agit de la moyenne sur deux minutes (120 secondes). Donc le prix moyen est 114,632/120 = 0,955.

Ce calcul de prix est la raison pour laquelle nous avons besoin de 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 }
Copier

Enfin, mettez à jour les variables globales et émettez 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) {
Copier

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 distribués soit aux fournisseurs de liquidités, soit à une adresse spécifiée par l'usine en tant que frais de protocole, qui paie Uniswap pour leurs 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 que lors de chaque transaction.

1 address feeTo = IUniswapV2Factory(factory).feeTo();
2 feeOn = feeTo != address(0);
Copier

Regardez les frais de destination de l'usine. S'ils sont de zéro, alors il n'y aura pas de frais de protocole et donc nul besoin de les calculer.

1 uint _kLast = kLast; // gas savings
Copier

La variable d'état kLast est située dans le stockage et aura donc une valeur entre différents appels au contrat. L'accès au stockage est beaucoup plus onéreux que l'accès à la mémoire volatile qui est libérée lorsque l'appel de fonction au contrat se termine. Ainsi, nous utilisons une variable interne pour faire des économies sur le gaz.

1 if (feeOn) {
2 if (_kLast != 0) {
Copier

Les fournisseurs de liquidités obtiennent leur part par simple appréciation de leurs jetons de liquidité. Mais les frais de protocole nécessitent de nouveaux jetons de liquidité à frapper et à fournir à l'adresse feeTo.

1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
2 uint rootKLast = Math.sqrt(_kLast);
3 if (rootK > rootKLast) {
Copier

S'il y a de nouvelles liquidités sur lesquelles percevoir des frais de protocole. Vous pourrez voir la fonction 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;
Copier

Ce calcul complexe des frais est expliqué dans le livre blanc(opens in a new tab) en page 5. Nous savons qu'entre le moment où le temps kLast a été calculé et le moment présent, aucune liquidité n'a été ajoutée ou supprimée (car nous n'exécutons ce calcul qu'à chaque fois que la liquidité est ajoutée ou supprimée, avant qu'il ne change réellement). Par conséquent, tout changement dans reserve0 * reserve1 doit venir des frais de transaction (sans eux, nous conserverons la constante reserve0 * reserve1).

1 if (liquidity > 0) _mint(feeTo, liquidity);
2 }
3 }
Copier

Utilisez 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 }
Copier

S'il n'existe pas de frais, réglez 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 récupère ce remboursement lorsque c'est possible.

Fonctions accessibles en externe

Notez que bien que toute transaction ou contrat peut 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 concernant la paire d'échange, mais vous pourriez perdre des valeurs par erreur.

frapper
1 // this low-level function should be called from a contract which performs important safety checks
2 function mint(address to) external lock returns (uint liquidity) {
Copier

Cette fonction est appelée lorsqu'un fournisseur de liquidités ajoute de la liquidité à la réserve. Elle frappe des jetons de liquidités supplémentaires en 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 (ainsi personne d'autre ne serait en mesure de soumettre une transaction sollicitant la nouvelle liquidité avant le propriétaire légitime).

1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
Copier

C'est ainsi que doivent être lus les résultats d'une fonction Solidity qui restitue plusieurs valeurs. Nous rejetons les dernières valeurs retournées, 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);
Copier

Récupèrez les soldes courants et vérifiez le montant ajouté à chaque type de jeton.

1 bool feeOn = _mintFee(_reserve0, _reserve1);
Copier

Calculez les frais de protocole à percevoir, le cas échéant, et minez les jetons de liquidité en conséquence. Parce que les paramètres de _mintFee sont les anciennes valeurs de réserve, les frais sont calculés avec précision uniquement sur la base des modifications apportées au pool en raison des frais.

1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
2 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 tokens
Copier

S'il s'agit du premier dépôt, créez des jetons MINIMUM_LIQUIDITY et envoyez-les à l'adresse zéro pour les verrouiller. Ils ne peuvent jamais être rachetés, ce qui signifie que la réserve ne sera jamais complètement vide (d'une certaine façon cela nous évite une division par zéro). La valeur MINIMUM_LIQUIDITY est d'un millier, ce qui implique que la plupart des ERC-20 sont subdivisés en unités de 10^-18'e d'un jeton, étant donné que la division de l'ETH en wei est de 10^-15 de la valeur d'un jeton unique. Ce n'est pas un coût élevé.

Lors du premier dépôt, nous ne connaissons pas la valeur relative des deux jetons, donc nous multiplions simplement les montants et prenons une racine carrée, en supposant que le dépôt nous donne la même valeur pour les deux jetons.

Nous pouvons nous y fier parce qu'il est dans l'intérêt du déposant de fournir une valeur égale pour éviter de perdre de la valeur à l'arbitrage. Imaginons que la valeur des deux jetons est identique mais que notre déposant a déposé quatre fois plus de Jetons1 que de Jetons0. Un trader peut s'appuyer sur le fait que l'échange de paire laisse supposer qu'il est plus utile de retirer de la valeur du Jeton0.

Evénementréserve0réserve1réserve0 * réserve1Valeur du pool (réserve0 + réserve1)
Configuration initiale83225640
Le Trader dépose dépose 8 jetons Jeton0 et récupère 16 Jetons1161625632

Comme vous pouvez le constater, le trader a gagné 8 jetons supplémentaires qui viennent d'une réduction de la valeur du pool, affectant le déposant qui le possède.

1 } else {
2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
Copier

À 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 dans les deux. S'ils ne le font pas, nous leur donnons en guise de punition des jetons de liquidité basés sur la valeur la plus basse.

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 du changement dans reserve0*reserve1 et la valeur du jeton de liquidité ne change pas (sauf si nous obtenons 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, donc il ne produit aucun jeton de liquidité).

Événementréserve0réserve1réserve0 * réserve1Valeur du pool (réserve0 + réserve1)Jetons de liquidité frappés pour ce dépôtJetons de liquidité totauxValeur de chaque jeton de liquidité
Configuration initiale8,0008,0006416,000882,000
Dépôt de quatre de chaque type12,00012,00014424,0004122,000
Dépôt de deux de chaque type14,00014,00019628,0002142,000
Dépôt de valeurs inégales18,00014,00025232,000014~2,286
Après arbitrage~15,874~15,874252~31,748014~2,267
1 }
2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
3 _mint(to, liquidity);
Copier

Utilisez la fonction UniswapV2ERC20._mint pour créer les jetons de liquidité supplémentaires et les attribuer au bon compte.

1
2 _update(balance0, balance1, _reserve0, _reserve1);
3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
4 emit Mint(msg.sender, amount0, amount1);
5 }
Copier

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 checks
2 function burn(address to) external lock returns (uint amount0, uint amount1) {
Copier

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 savings
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 uint balance0 = IERC20(_token0).balanceOf(address(this));
5 uint balance1 = IERC20(_token1).balanceOf(address(this));
6 uint liquidity = balanceOf[address(this)];
Copier

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 _mintFee
3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
Copier

Le fournisseur de liquidités reçoit la même valeur des deux jetons. De cette façon, nous ne changeons 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));
6
7 _update(balance0, balance1, _reserve0, _reserve1);
8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
9 emit Burn(msg.sender, amount0, amount1, to);
10 }
11
Afficher tout
Copier

Le reste de la fonction burn est l'image miroir de la fonction mint ci-dessus.

échanger
1 // this low-level function should be called from a contract which performs important safety checks
2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
Copier

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 savings
3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
4
5 uint balance0;
6 uint balance1;
7 { // scope for _token{0,1}, avoids stack too deep errors
Copier

Les variables locales peuvent être stockées en mémoire ou, si elles ne sont pas trop nombreuses, directement sur la pile. Si nous pouvons limiter le nombre, nous utiliserons la pile pour utiliser moins de gaz. Pour plus de détails, consultez the yellow paper, the formal Ethereum specifications(opens in a new tab), p. 26, equation 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 tokens
5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
Copier

Ce transfert est optimiste puisque nous transférons avant d'être sûrs que toutes les conditions sont remplies. Ceci est acceptable dans Ethereum parce que si les conditions ne sont pas remplies plus tard lors de l'appel, l'opération ainsi que les changements réalisés seront annulés.

1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
Copier

Informez si celà est demandé, le bénéficiaire du swap.

1 balance0 = IERC20(_token0).balanceOf(address(this));
2 balance1 = IERC20(_token1).balanceOf(address(this));
3 }
Copier

Récupèrez les soldes actuels. Le contrat périphérique nous envoie les jetons avant de nous appeler pour l'échange. Il est ainsi aisé de s'assurer que le contrat n'a pas été falsifié, cette vérification devant intervenir dans le contrat de base (parce que 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 errors
5 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');
Copier

Ceci est un test d'intégrité visant à s'assurer que nous ne perdons rien lors de l'échange. En aucune circonstance, un échange ne devrait réduire la reserve0*reserve1. C'est également dans ce cntexte 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 soustraits des 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 actuelle des réserves K.

1 }
2
3 _update(balance0, balance1, _reserve0, _reserve1);
4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
5 }
Copier

Mettre à jour réserve0 et réserve1, et si nécessaire les accumulateurs de prix, l'horodatage et émettre un événement.

Synchroniser ou ignorer

Il est possible que les soldes réels se désynchronisent des réserves que l'échange de la paire aura généré. Il n'y a aucun moyen de retirer des jetons sans l'accord du contrat, mais pour les dépôts c'est une autre affaire. Un compte peut transférer des jetons à l'échange sans avoir appelé mint ou swap.

Dans ce cas, il y a deux solutions :

  • sync, mettre à jour les réserves en tenant compte des soldes actuels
  • skim, retirer le montant supplémentaire. Notez que tout compte est autorisé à appeler skim parce que 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 reserves
2 function skim(address to) external lock {
3 address _token0 = token0; // gas savings
4 address _token1 = token1; // gas savings
5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
7 }
8
9
10
11 // force reserves to match balances
12 function sync() external lock {
13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
14 }
15}
Afficher tout
Copier

UniswapV2Factory.sol

Ce contrat(opens in a new tab) crée l es échanges de paires.

1pragma solidity =0.5.16;
2
3import './interfaces/IUniswapV2Factory.sol';
4import './UniswapV2Pair.sol';
5
6contract UniswapV2Factory is IUniswapV2Factory {
7 address public feeTo;
8 address public feeToSetter;
Copier

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;
Copier

Ces variables gardent une trace des paires, des échanges entre deux types de jetons.

La première, getPair, est un mapping qui identifie un contrat d'échange de paires basé sur les deux jetons ERC-20 échangés. Les jetons ERC-20 sont identifiés par les adresses des contrats qui les implémentent. Ainsi, les clés et la valeur sont toutes des adresses. Pour obtenir l'adresse de l'échange de paires qui vous permet de convertir des tokenA en tokenB, vous utilisez getPair[<tokenA address>][<tokenB address>] (ou l'inverse).

La seconde variable, allPairs, est un tableau qui inclut toutes les adresses d'échanges de paires créées par cette usine. Sur Ethereum, vous ne pouvez pas itérer le contenu d'un mapping ou obtenir une liste de toutes les clés, ainsi cette variable est la seule façon de savoir quels échanges sont gérés par cette usine.

Remarque : Si vous ne pouvez pas itérer sur toutes les clés d'un mapping c'est parce que le stockage des données de contrat est coûteux, ainsi moins nous en utilisons, moins nous réalisons de changements et mieux ce sera. Vous pouvez créer des mappings qui supportent l'itération(opens in a new tab) mais qui nécessitent un stockage supplémentaire pour une liste de clés. Dans la plupart des applications, vous n'avez pas besoin de ça.

1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);
Copier

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 la paire et le nombre total d'échanges gérés par l'usine.

1 constructor(address _feeToSetter) public {
2 feeToSetter = _feeToSetter;
3 }
Copier

La seule chose que le constructeur fait 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 }
Copier

Cette fonction restitue le nombre de paires d'échange.

1 function createPair(address tokenA, address tokenB) external returns (address pair) {
Copier

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);
Copier

Nous souhaitons que l'adresse du nouvel échange soit déterminable de sorte qu'elle puisse être calculée à l'avance hors chaîne (cela peut être utile pour les transactions de couche 2). Pour cela, nous devons avoir les adresses de jetons dans un ordre cohérent indépendant de l'ordre dans lequel nous les avons reçus. Aussi les trions-nous ici.

1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
Copier

Les grands pools de liquidités sont meilleurs que les petits parce qu'ils proposent des prix plus stables. Nous ne voulons pas disposer de 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;
Copier

Pour créer un nouveau contrat, nous avons besoin du code qui va le créer (tant la fonction constructeur que le code qui va écrire en mémoire le bytecode EVM du contrat effectif). Normalement dans Solidity nous utilisons addr = new <name of contract>(<constructor parameters>) et le compilateur s'occupe de tout pour nous. Pour obtenir une adresse de contrat déterminable, nous devons toutefois 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 et il était donc nécessaire d'obtenir manuellement le code. Ce n'est plus un problème car Solidity prend désormais 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 }
Copier

Lorsqu'un opcode n'est pas encore pris en charge par Solidity, nous pouvons l'appeler en utilisant l'assemblage en ligne(opens in a new tab).

1 IUniswapV2Pair(pair).initialize(token0, token1);
Copier

Appelez la fonction initialize pour dire au nouvel échange quels sont les deux jetons qu'il échange.

1 getPair[token0][token1] = pair;
2 getPair[token1][token0] = pair; // populate mapping in the reverse direction
3 allPairs.push(pair);
4 emit PairCreated(token0, token1, pair, allPairs.length);
5 }
Copier

Enregistrez les informations de la nouvelle paire dans les variables d'état et émettez un événement pour informer tout le monde du nouvel échange de paires.

1 function setFeeTo(address _feeTo) external {
2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
3 feeTo = _feeTo;
4 }
5
6 function setFeeToSetter(address _feeToSetter) external {
7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
8 feeToSetter = _feeToSetter;
9 }
10}
Afficher tout
Copier

Ces deux fonctions permettent à feeSetter de contrôler le destinataire des frais (le cas échéant) et d'orienter feeSetter vers une nouvelle adresse.

UniswapV2ERC20.sol

Ce contrat(opens in a new tab) implemente le jeton de liquidité ERC-20. Il est identique au contrat OpenZeppelin ERC-20. De fait, je n'expliquerai que la partie qui est différente, à savoir 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. Vous ne pouvez donc rien faire avec. Pour éviter ce problème, vous pouvez opter pour des méta-transactions(opens in a new tab). Le propriétaire des jetons signe une transaction qui permet à quelqu'un d'autre de retirer des jetons hors chaîne et de les envoyer via Internet à un destinataire. Le destinataire disposant d'ETH soumet ensuite le permis pour le compte 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;
Copier

Ce hachage est l'identifiant pour le 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;
Copier

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 répétition(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 supposons qu'il est invalide.

1 constructor() public {
2 uint chainId;
3 assembly {
4 chainId := chainid
5 }
Copier

Ceci est le code pour récupérer l'identifiant de la chaîne(opens in a new tab). Il utilise une langue d'assemblage EVM appelé Yul(opens in a new tab). Notez que dans la version actuelle de Yul, vous devez utiliser chainid() et non pas 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 tout
Copier

Calculer 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 {
Copier

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');
Copier

N'acceptez pas 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 );
Copier

abi.encodePacked(...) est le message que nous attendons de recevoir. Nous savons ce que le nonce devrait être. Il n'y a donc nul besoin de l'obtenir sous forme de paramètre.

L'algorithme de signature Ethereum a besoin de 256 bits pour signer. aussi, utilisons-nous la fonction de hachage keccak256.

1 address recoveredAddress = ecrecover(digest, v, r, s);
Copier

À partir du condensé et de la signature, nous pouvons obtenir l'adresse qui l'a signé en utilisant ecrecovery(opens in a new tab).

1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
2 _approve(owner, spender, value);
3 }
4
Copier

Quand tout est en ordre, traitez ceci comme un ERC-20 approve(opens in a new tab).

Les contrats périphériques

Les contrats périphériques sont l'API (interface du programme d'application) pour 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 des valeurs si vous faites une erreur. Les contrats de base ne contiennent que des tests pour éviter toute escroquerie au contrat, pas de tests d'intégrité pour qui que ce soit d'autre. Ceux-ci sont au niveau des périphériques pour pouvoir être mis à jour au besoin.

UniswapV2Router01.sol

Ce contrat(opens in a new tab) présente des dangers et ne doit plus être utilisé(opens in a new tab). Heureusement, les contrats périphériques sont dénués d'état et ne détiennent aucun actif. Il est ainsi facile de les déprécier et de suggérer aux gens d'utiliser à la place UniswapV2Router02.

UniswapV2Router02.sol

Dans la plupart des cas, vous utiliseriez Uniswap par le biais de ce contrat(opens in a new tab). Vous pouvez voir comment l'utiliser ici(opens in a new tab).

1pragma solidity =0.6.6;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
5
6import './interfaces/IUniswapV2Router02.sol';
7import './libraries/UniswapV2Library.sol';
8import './libraries/SafeMath.sol';
9import './interfaces/IERC20.sol';
10import './interfaces/IWETH.sol';
Afficher tout
Copier

Nous avons déjà rencontrés auparavant la plupart d'entre eux ou ils sont assez évidents. La seule exception est IWETH.sol. Uniswap v2 permet l'échange de n'importe quelle paire de jetons ERC-20 mais l'éther (ETH), en lui-même, n'est pas un jeton ERC-20. Il est antérieur à la norme et est transféré par des mécanismes spécifiques. Pour activer l'utilisation d'ETH dans les contrats qui s'appliquent aux jetons ERC-20, les programmeurs ont l'habitude d'utiliser le contrat wrapped ether (WETH)(opens in a new tab). Vous envoyez ce contrat ETH, et il va frapper un montant équivalent de WETH. Vous pouvez également brûler WETH, et récupérer de l'ETH en retour.

1contract UniswapV2Router02 is IUniswapV2Router02 {
2 using SafeMath for uint;
3
4 address public immutable override factory;
5 address public immutable override WETH;
Copier

Le routeur a besoin de savoir quelle usine utiliser, et pour les transactions qui nécessitent 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 tendre vers des contrats moins sûrs.

1 modifier ensure(uint deadline) {
2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
3 _;
4 }
Copier

Ce modificateur permet de s'assurer que les opérations limitées dans le temps (« faire X avant le moment Y si possible ») ne se produisent pas après la limite de temps impartie.

1 constructor(address _factory, address _WETH) public {
2 factory = _factory;
3 WETH = _WETH;
4 }
Copier

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 contract
3 }
Copier

Cette fonction est appelée lorsque nous échangeons des jetons du contrat WETH en ETH. Seul le contrat WETH que nous utilisons est autorisé à faire cela.

Ajouter des liquidités

Ces fonctions ajoutent des jetons à l'échange de paires, ce qui augmente la réserve de liquidités.

1
2 // **** ADD LIQUIDITY ****
3 function _addLiquidity(
Copier

Cette fonction est utilisée pour calculer le nombre de jetons A et B qui doivent être déposés dans l'échange de paires.

1 address tokenA,
2 address tokenB,
Copier

Voici les adresses des contrats de jetons ERC-20.

1 uint amountADesired,
2 uint amountBDesired,
Copier

Ce sont les montants que le fournisseur de liquidités veut déposer. Ils sont également les montants maximum de A et de B à déposer.

1 uint amountAMin,
2 uint amountBMin
Copier

Ce sont les montants minimums acceptables pour le dépôt. Si la transaction ne peut avoir lieu avec ces montants ou plus, annulez-la. Si vous ne souhaitez pas de 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 actuel. Si le taux de change fluctue trop, ce peut être en raison de nouvelles qui changent les valeurs sous-jacentes et les fournisseurs peuvent alors vouloir décider manuellement ce qu'il faut faire.

Par exemple, imaginez un cas où le taux de change est d'un pour un et où le fournisseur de liquidités spécifie ces valeurs :

ParamètreValeur
amountADesired1000
amountBDesired1000
amountAMin900
amountBMin800

Tant que le taux de change reste compris entre 0,9 et 1,25, la transaction aura lieu. Si le taux de change sort de cette fourchette, la transaction sera annulée.

Cette précaution s'explique par le fait que les transactions ne sont pas immédiates, vous les soumettez et au bout du compte un validateur va les inclure dans un bloc (à moins que le prix du gaz soit particulièrement bas, auquel cas vous devrez soumettre, à la place, une autre transaction avec le même nonce et un prix de gaz plus élevé). 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) {
Copier

La fonction retourne les montants que le fournisseur de liquidités doit déposer pour avoir un taux égal au taux actuel entre les réserves.

1 // create the pair if it doesn't exist yet
2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);
4 }
Copier

S'il n'existe pas encore d'échange pour cette paire de jetons, créez-la.

1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
Copier

Récupérez les réserves actuelles dans la paire.

1 if (reserveA == 0 && reserveB == 0) {
2 (amountA, amountB) = (amountADesired, amountBDesired);
Copier

Si les réserves actuelles sont vides alors il s'agit d'un nouvel échange de paires. Les montants à déposer devraient ê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);
Copier

Si nous souhaitons connaître quels sont ces montants, sachez que le montant optimal est obtenu à l'aide decette fonction(opens in a new tab). L'objectif est d'avoir 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);
Copier

Si amountBOptimal est plus petit que le montant que le fournisseur de liquidités veut déposer, cela signifie que le jeton B est à ce stade plus précieux que ne le pense le déposant de liquidité. Ainsi, un montant plus bas est 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);
Copier

Si le montant optimal de B est supérieur au montant désiré de B, cela signifie que les jetons B sont moins précieux actuellement que ne le pense le déposant de liquidités. Ainsi, un montant plus élevé est requis. Cependant, le montant souhaité est un maximum et nous ne pouvons donc pas aller jusque-là. Nous calculons donc le nombre optimal de jetons A pour le montant souhaité de jetons B.

En mettant tout cela ensemble, nous obtenons ce graphique. Supposons que vous essayez de déposer un millier de jetons A (ligne bleue) et un millier de jetons B (ligne rouge). L'axe x est le taux de change, A/B. Si x=1, ils sont égaux en valeur et vous déposez mille de chaque. Si x=2, A est 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 alors inversée avec mille jetons A et cinq cents jetons B.

Graph

Vous pouvez déposer des liquidités directement dans le contrat principal (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. Ainsi, vous risquez 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 indique le montant que vous devez déposer et le dépose immédiatement. Ainsi, le taux de change n'évolue 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 deadline
Afficher tout
Copier

Cette fonction peut être appelée par une transaction de dépôt de liquidités. La plupart des paramètres sont les mêmes que pour _addLiquidity ci-dessus, à deux exceptions près :

. to est l'adresse qui reçoit les nouveaux jetons de liquidité minés pour montrer la part du pool que détient le fournisseur de liquidités. deadline est une limite de temps sur 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);
Copier

Nous calculons les montants à déposer et trouvons ensuite l'adresse de la réserve 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);
Copier

Transférez les bons montants de jetons de l'utilisateur à l'échange de paire.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 }
Copier

En retour, donnez des jetons de liquidité à l'adresse to en vue de la détention partielle de la réserve. La fonction mint du contrat de base détermine de combien de jetons supplémentaires elle dispose (par rapport à ce dont elle disposait 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,
Copier

Lorsqu'un fournisseur de liquidités veut fournir des liquidités à un échange de paire Jeton/ETH, il y a quelques différences. Le contrat gère l'encapsulage d'ETH pour le fournisseur de liquidités. Il n'est pas nécessaire de spécifier combien d'ETH l'utilisateur veut déposer, parce que l'utilisateur les envoie simplement avec la transaction (le montant est disponible dans msg.value).

1 uint amountTokenMin,
2 uint amountETHMin,
3 address to,
4 uint deadline
5 ) 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 amountETHMin
13 );
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 tout
Copier

Pour déposer l'ETH le contrat va l'envelopper dans WETH, puis transfère le WETH dans la paire. Notez que le transfert est encapsulé dans un assert. Cela signifie que si le transfert échoue, cet appel de contrat échoue également et qu'ainsi l'encapsulage ne se produit pas.

1 liquidity = IUniswapV2Pair(pair).mint(to);
2 // refund dust eth, if any
3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
4 }
Copier

L'utilisateur nous a déjà envoyé l'ETH. Ainsi, s'il existe des reliquats (parce que l'autre jeton est moins précieux que ce que l'utilisateur pense), nous devons effectuer un remboursement.

Retirer 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 deadline
10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
Afficher tout
Copier

Le cas le plus simple pour supprimer les liquidités. Il y a un montant minimum pour chaque jeton que le fournisseur de liquidités convient d'accepter et cela doit se produire avant la date limite.

1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
Copier

La fonction burn du contrat de base permet de payer les jetons en retour à l'utilisateur.

1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
Copier

Lorsqu'une fonction retourne plusieurs valeurs, mais que nous ne sommes intéressés que par certaines d'entre elles, nous récupérons 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);
Copier

Traduit les montants de la façon dont le contrat de base les renvoie (jeton d'adresse inférieure d'abord) à la manière dont l'utilisateur les attend (correspondant au jetonA et jetonB).

1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
3 }
Copier

Il est possible de faire le transfert d'abord puis de vérifier qu'il est légitime, 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 deadline
8 ) 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 deadline
17 );
18 TransferHelper.safeTransfer(token, to, amountToken);
19 IWETH(WETH).withdraw(amountETH);
20 TransferHelper.safeTransferETH(to, amountETH);
21 }
Afficher tout
Copier

Retirer la liquidité des ETH se fait presque de la même manière sauf que nous recevons les jetons WETH et que nous les rachetons pour que des ETH soient restitués 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 s
10 ) 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 }
16
17
18 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 s
26 ) 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 tout
Copier

Ces fonctions relayent les méta-transactions pour permettre aux utilisateurs sans éther de se retirer du pool en utilisant le mécanisme du permis.

1
2 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
3 function removeLiquidityETHSupportingFeeOnTransferTokens(
4 address token,
5 uint liquidity,
6 uint amountTokenMin,
7 uint amountETHMin,
8 address to,
9 uint deadline
10 ) public virtual override ensure(deadline) returns (uint amountETH) {
11 (, amountETH) = removeLiquidity(
12 token,
13 WETH,
14 liquidity,
15 amountTokenMin,
16 amountETHMin,
17 address(this),
18 deadline
19 );
20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
21 IWETH(WETH).withdraw(amountETH);
22 TransferHelper.safeTransferETH(to, amountETH);
23 }
24
Afficher tout
Copier

Cette fonction peut être utilisée pour les jetons associés à 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 nous retirer et ensuite obtenir le solde.

1
2
3 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 s
11 ) 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, deadline
17 );
18 }
Afficher tout
Copier

La fonction finale combine les frais de stockage avec les méta-transactions.

Échanger

1 // **** SWAP ****
2 // requires the initial amount to have already been sent to the first pair
3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
Copier

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++) {
Copier

Au moment d'écrire ces lignes, il y a 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, en cet instant précis, ne dispose que de 0,1 % de ce nombre de comptes(opens in a new tab). Les fonctions d'échange supportent le concept du 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.

  1. La situation initiale
  2. Un négociant vend 24,695 jetons A et récupère en échange 25,305 jetons B.
  3. Le trader vend 24,695 jetons B contre 25,305 jetons C, et garde environ 0,61 jeton B comme bénéfice.
  4. 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
1A : 1 000 B : 1 050 A/B=1,05B : 1 000 C : 1 050 B/C=1,05A : 1 050 C : 1 000 C/A=1,05
2A : 1 024,695 B : 1 024,695 A/B=1B : 1 000 C : 1 050 B/C=1,05A : 1 050 C : 1 000 C/A=1,05
3A : 1 024,695 B : 1 024,695 A/B=1B : 1 024,695 C : 1 024,695 B/C=1A : 1 050 C : 1 000 C/A=1,05
4A : 1 024,695 B : 1 024,695 A/B=1B :1 024,695 C : 1 024,695 B/C=1A : 1 024,695 C : 1 024,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];
Copier

Obtenez la paire que nous traitons actuellement, triez-la (pour l'utiliser avec la paire) et obtenez le montant prévu de sortie.

1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
Copier

Obtenez les montants prévus, triés de la façon dont l'échange de paire est souhaité.

1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
Copier

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.

1
2 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
3 amount0Out, amount1Out, to, new bytes(0)
4 );
5 }
6 }
Copier

Effectuez un appel pour l'échange de paire afin d'é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(
Copier

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,
Copier

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 sur Solidity peut être stocké soit dans memory, soit dans 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 ci-dessus avec _swap, alors les paramètres doivent être stockés dans 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 moyen 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 deadline
3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
Copier

Les valeurs retournées 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');
Copier

Calculez le montant à acheter pour chaque échange. Si le résultat est inférieur au minimum que ce 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 }
Copier

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 paire 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 deadline
7 ) 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 tout
Copier

La 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 indemnité lui permettant de les transférer.

1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
2 external
3 virtual
4 override
5 payable
6 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 }
16
17
18 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
19 external
20 virtual
21 override
22 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 }
35
36
37
38 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
39 external
40 virtual
41 override
42 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 }
55
56
57 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
58 external
59 virtual
60 override
61 payable
62 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 any
72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
73 }
Afficher tout
Copier

Ces 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 pair
3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
Copier

C'est la fonction interne pour échanger les jetons associés à 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 { // scope to avoid stack too deep errors
8 (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 tout
Copier

En raison des frais de transfert, nous ne pouvons pas compter sur la fonction getAmountsOut pour nous dire combien nous tirons de chaque transfert (la façon dont 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 s'achève parce qu'il n'y a pas assez à la fin pour atteindre le minimum requis) cela finirait par coûter plus en gaz. Les jetons de frais de transfert sont assez rares, donc bien que nous devions les prendre en compte, il n'est pas nécessaire de supposer que tous les swaps passent par au moins un d'entre eux.

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 }
7
8
9 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
10 uint amountIn,
11 uint amountOutMin,
12 address[] calldata path,
13 address to,
14 uint deadline
15 ) external virtual override ensure(deadline) {
16 TransferHelper.safeTransferFrom(
17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
18 );
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 }
26
27
28 function swapExactETHForTokensSupportingFeeOnTransferTokens(
29 uint amountOutMin,
30 address[] calldata path,
31 address to,
32 uint deadline
33 )
34 external
35 virtual
36 override
37 payable
38 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 }
51
52
53 function swapExactTokensForETHSupportingFeeOnTransferTokens(
54 uint amountIn,
55 uint amountOutMin,
56 address[] calldata path,
57 address to,
58 uint deadline
59 )
60 external
61 virtual
62 override
63 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]), amountIn
68 );
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 tout
Copier

Ce sont les mêmes variants utilisés pour les jetons normaux, mais ils appellent à la place _swapSupportingFeeOnTransferTokens.

1 // **** LIBRARY FUNCTIONS ****
2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
3 return UniswapV2Library.quote(amountA, reserveA, reserveB);
4 }
5
6 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
7 public
8 pure
9 virtual
10 override
11 returns (uint amountOut)
12 {
13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
14 }
15
16 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
17 public
18 pure
19 virtual
20 override
21 returns (uint amountIn)
22 {
23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
24 }
25
26 function getAmountsOut(uint amountIn, address[] memory path)
27 public
28 view
29 virtual
30 override
31 returns (uint[] memory amounts)
32 {
33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);
34 }
35
36 function getAmountsIn(uint amountOut, address[] memory path)
37 public
38 view
39 virtual
40 override
41 returns (uint[] memory amounts)
42 {
43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);
44 }
45}
Afficher tout
Copier

Ces fonctions ne sont que des proxy qui appellent les fonctions 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, ce n'est plus pertinent.

Les bibliothèques

La bibliothèque SafeMath(opens in a new tab) est bien documentée et il n'y a pas lieu donc ici de le faire.

Mathématiques

Cette bibliothèque contient des fonctions mathématiques qui ne sont pas habituelement nécessaires dans le code Solidity. Elles ne font donc pas partie du langage.

1pragma solidity =0.5.16;
2
3// a library for performing various math operations
4
5library Math {
6 function min(uint x, uint y) internal pure returns (uint z) {
7 z = x < y ? x : y;
8 }
9
10 // babylonian method (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 tout
Copier

Commencez 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 spéciaux).

1 while (x < z) {
2 z = x;
3 x = (y / x + x) / 2;
Copier

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és par l'estimation précédente. Répétez jusqu'à ce que la nouvelle estimation ne soit pas inférieure à celle existante. Pour de plus amples informations : voir ici(opens in a new tab).

1 }
2 } else if (y != 0) {
3 z = 1;
Copier

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}
Copier

Fractions de points fixes (UQ112x112)

Cette bibliothèque gère les fractions qui ne font normalement pas partie de l'arithmétique Ethereum. Elle réalise cela en encodant le nombre x en x*2^112. Cela nous permet d'utiliser l'addition originale et les opcodes de soustraction sans aucun changement.

1pragma solidity =0.5.16;
2
3// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))
4
5// range: [0, 2**112 - 1]
6// resolution: 1 / 2**112
7
8library UQ112x112 {
9 uint224 constant Q112 = 2**112;
Afficher tout
Copier

Q112 est l'encodage pour un.

1 // encode a uint112 as a UQ112x112
2 function encode(uint112 y) internal pure returns (uint224 z) {
3 z = uint224(y) * Q112; // never overflows
4 }
Copier

Parce que 'y' est uint112, le maximum peut-être 2^112-1. Ce nombre peut toujours être encodé en tant que UQ112x112.

1 // divide a UQ112x112 by a uint112, returning a UQ112x112
2 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
3 z = x / uint224(y);
4 }
5}
Copier

Si nous divisons deux valeurs UQ112x112, le résultat ne sera 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.

Bibliothèque UniswapV2

Cette bibliothèque n'est utilisée que par les contrats périphériques

1pragma solidity >=0.5.0;
2
3import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
4
5import "./SafeMath.sol";
6
7library UniswapV2Library {
8 using SafeMath for uint;
9
10 // returns sorted token addresses, used to handle return values from pairs sorted in this order
11 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 tout
Copier

Trie les deux jetons par leurs adresses afin que nous puissions obtenir l'adresse de leur échange en paire. Ceci est nécessaire car sinon nous aurions deux possibilités, une pour les paramètres A,B et un autre pour les paramètres B,A, impliquant deux échanges au lieu d'un.

1 // calculates the CREATE2 address for a pair without making any external calls
2 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' // init code hash
9 ))));
10 }
Afficher tout
Copier

Cette fonction calcule l'adresse de l'échange en paire pour les deux jetons. Ce contrat est créé en utilisant l'opcode CREATE2(opens in a new tab) et ainsi nous pouvons calculer l'adresse utilisant le même algorithme si nous connaissons les paramètres qu'il utilise. C'est beaucoup moins cher que de demander à l'usine.

1 // fetches and sorts the reserves for a pair
2 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 }
Copier

Cette fonction retourne les réserves des deux jetons que l'échange en paire possède. Notez qu'il peut recevoir les jetons dans l'ordre et les trier pour un usage interne.

1 // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
2 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 }
Copier

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 // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
Copier

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 est inférieur. Cette fonction calcule le montant après les frais de change.

1
2 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 }
Copier

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, avec le même effet.

1 // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
2 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 }
Copier

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.

1
2 // performs chained getAmountOut calculations on any number of pairs
3 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 }
12
13 // performs chained getAmountIn calculations on any number of pairs
14 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 tout
Copier

Ces deux fonctions permettent d'identifier les valeurs lorsqu'il est nécessaire de passer par plusieurs échanges de paires.

Assistant de transfert

Cette bibliothèque(opens in a new tab) ajoute des vérifications de réussites des transferts ERC-20 et Ethereum pour traiter une annulation et une valeur retournée false de la même façon.

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3pragma solidity >=0.6.0;
4
5// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
6library TransferHelper {
7 function safeApprove(
8 address token,
9 address to,
10 uint256 value
11 ) internal {
12 // bytes4(keccak256(bytes('approve(address,uint256)')));
13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
14
Afficher tout
Copier

Nous pouvons appeler un autre contrat de deux manières :

1 require(
2 success && (data.length == 0 || abi.decode(data, (bool))),
3 'TransferHelper::safeApprove: approve failed'
4 );
5 }
Copier

Pour des raisons de compatibilité ascendante avec un jeton qui a été créé avant la norme ERC-20, un appel ERC-20 peut échouer soit en revenant (dans ce cas success sera false) soit en réussissant et en retournant une valeur false (dans ce cas il y aura une donnée de sortie et si vous la décodez comme un booléen, vous obtenez false).

1
2
3 function safeTransfer(
4 address token,
5 address to,
6 uint256 value
7 ) 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 tout
Copier

Cette fonction implémente la fonctionnalité transfert de ERC-20(opens in a new tab), qui permet à un compte de dépenser la provision fournie par un autre compte.

1
2 function safeTransferFrom(
3 address token,
4 address from,
5 address to,
6 uint256 value
7 ) 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 tout
Copier

Cette fonction implémente la fonctionnalité transferFrom de ERC-20(opens in a new tab), qui permet à un compte de dépenser la provision fournie par un autre compte.

1
2 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}
Copier

Cette fonction transfère de l'éther vers un compte. Tout appel à un autre contrat peut tenter d'envoyer l'éther en question. 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 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 courts d'échantillons) et êtes plus à même d'écrire des contrats pour vos propres utilisations.

Et maintenant, à vous d'écrire quelque chose d'utile et de nous étonner.

Dernière modification: @MATsxm(opens in a new tab), 2 avril 2024

Ce tutoriel vous a été utile ?