Visite guidée du contrat Uniswap-v2
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 :
- Échanger différents jetons
- Ajouter des liquidités sur le marché et obtenir des récompenses sous forme de jetons de liquidité ERC
- 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
- Fournir au compte périphérique une provision correspondant au montant à échanger.
- 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)
- Identifier les montants qui doivent être négociés à chaque échange tout au long du chemin.
- 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)
- Vérifier que le contrat de base n'est pas faussé et qu'il dispose de suffisamment de liquidités après l'échange.
- 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.
- Envoyer les jetons de sortie à la destination.
- Appeler
_update
pour mettre à jour les montants de la réserve
Retour au 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)
Ajouter des liquidités
Appelant
- Fournir au compte périphérique une provision à ajouter à la réserve de liquidités.
- Appeler l'une des fonctions
addLiquidity
du 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 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.
- 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
_update
pour mettre à jour les montants de la réserve
Retirer la 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
removeLiquidity
du contrat périphérique.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Envoyer les jetons de liquidité à l'échange de paire
Dans le contrat de base (UniswapV2Pair.sol)
- 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.
- Brûler les jetons de liquidité
- 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;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 toutCopier
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 getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReservesCopier
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 getReservesCopier
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 eventCopier
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énement | reserve0 | reserve1 | reserve0 * reserve1 | 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 pcontre 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 |
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 :
- Rétablir. Lorsque l'appel à un contrat externe indique que la valeur de retour booléen est
false
- 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 to8 );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 deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 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 accumulators2 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 desired3 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 desired2 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énement | réserve0 | réserve1 | horodatage | Taux de change marginal (réserve1 / réserve0) | DernierprixCumulé0 |
---|---|---|---|---|---|
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 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 savingsCopier
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 checks2 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 savingsCopier
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 _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 tokensCopier
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énement | réserve0 | réserve1 | réserve0 * réserve1 | Valeur du pool (réserve0 + réserve1) |
---|---|---|---|---|
Configuration initiale | 8 | 32 | 256 | 40 |
Le Trader dépose dépose 8 jetons Jeton0 et récupère 16 Jetons1 | 16 | 16 | 256 | 32 |
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é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 | Jetons de liquidité totaux | 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 valeurs inégales | 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);Copier
Utilisez la fonction UniswapV2ERC20._mint
pour créer les jetons de liquidité supplémentaires et les attribuer 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 }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 checks2 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 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)];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 _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');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));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 toutCopier
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 checks2 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 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 errorsCopier
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 tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokensCopier
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 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');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 }23 _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 actuelsskim
, retirer le montant supplémentaire. Notez que tout compte est autorisé à appelerskim
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 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 toutCopier
UniswapV2Factory.sol
Ce contrat(opens in a new tab) crée l es é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;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 sufficientCopier
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 direction3 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 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}Afficher toutCopier
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 := chainid5 }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 toutCopier
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 }4Copier
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;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 toutCopier
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;34 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 contract3 }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.
12 // **** 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 amountBMinCopier
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è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 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 yet2 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.
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 deadlineAfficher toutCopier
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 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 toutCopier
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 any3 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 deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {Afficher toutCopier
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 pair3 (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 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 toutCopier
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 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 toutCopier
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.
12 // **** 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 deadline10 ) 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 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24Afficher toutCopier
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.
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 toutCopier
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 pair3 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.
- La situation initiale
- Un négociant vend 24,695 jetons A et récupère en échange 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 : 1 024,695 B : 1 024,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 : 1 024,695 B : 1 024,695 A/B=1 | B : 1 024,695 C : 1 024,695 B/C=1 | A : 1 050 C : 1 000 C/A=1,05 |
4 | A : 1 024,695 B : 1 024,695 A/B=1 | B :1 024,695 C : 1 024,695 B/C=1 | A : 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.
12 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 deadline3 ) 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 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 toutCopier
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 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 toutCopier
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 pair3 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 errors8 (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 toutCopier
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 }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 toutCopier
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 }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 toutCopier
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;23// a library for performing various math operations45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // 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 toutCopier
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;23// a library for handling binary fixed point numbers (https://wikipedia.org/wiki/Q_(number_format))45// range: [0, 2**112 - 1]6// resolution: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;Afficher toutCopier
Q112
est l'encodage pour un.
1 // encode a uint112 as a UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // never overflows4 }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 UQ112x1122 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;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // returns sorted token addresses, used to handle return values from pairs sorted in this order11 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 toutCopier
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 calls2 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 hash9 ))));10 }Afficher toutCopier
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 pair2 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 asset2 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 asset2 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.
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 }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 asset2 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.
12 // performs chained getAmountOut calculations on any number of pairs3 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 // performs chained getAmountIn calculations on any number of pairs14 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 toutCopier
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-later23pragma solidity >=0.6.0;45// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return 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 toutCopier
Nous 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 d'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 }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
).
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 toutCopier
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.
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 toutCopier
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.
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}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: @wackerow(opens in a new tab), 2 avril 2024