Explication détaillée du contrat Uniswap-v2
Introduction
Uniswap v2 (opens in a new tab) peut créer un marché d'échange entre n'importe quels deux jetons ERC-20. Dans cet article, nous allons examiner le code source des contrats qui implémentent ce protocole et voir pourquoi ils sont écrits de cette façon.
Que fait Uniswap ?
Fondamentalement, il y a deux types d'utilisateurs : les fournisseurs de liquidité et les traders.
Les fournisseurs de liquidité fournissent à la réserve les deux jetons qui peuvent être échangés (nous les appellerons Token0 et Token1). En retour, ils reçoivent un troisième jeton qui représente la propriété partielle de la réserve, appelé jeton de liquidité.
Les traders envoient un type de jeton à la réserve et reçoivent l'autre (par exemple, ils envoient Token0 et reçoivent Token1) à partir de la réserve fournie par les fournisseurs de liquidité. Le taux de change est déterminé par le nombre relatif de Token0 et de Token1 que possède la réserve. De plus, la réserve prélève un petit pourcentage en guise de récompense pour la réserve de liquidité.
Lorsque les fournisseurs de liquidité veulent récupérer leurs actifs, ils peuvent brûler les jetons de la réserve 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 la v2 ? Pourquoi pas la v3 ?
Uniswap v3 (opens in a new tab) est une mise à jour beaucoup plus complexe que la v2. Il est plus facile d'apprendre d'abord la v2 avant de passer à la v3.
Contrats principaux vs contrats périphériques
Uniswap v2 est divisé en deux composants, un cœur et une périphérie. Cette division permet aux contrats principaux, qui détiennent les actifs et doivent donc être sécurisés, d'être plus simples et plus faciles à auditer. Toutes les fonctionnalités supplémentaires requises par les traders peuvent ensuite être fournies par les contrats périphériques.
Flux de données et de contrôle
Voici le flux de données et de contrôle qui se produit lorsque vous effectuez les trois actions principales d'Uniswap :
- Échanger entre différents jetons
- Ajouter de la liquidité au marché et être récompensé par des jetons de liquidité ERC-20 de la paire d'échange
- Brûler des jetons de liquidité ERC-20 et récupérer les jetons ERC-20 que la paire d'échange permet aux traders d'échanger
Échange
C'est le flux le plus courant, utilisé par les traders :
Appelant
- Fournir au compte périphérique une allocation du montant à échanger.
- Appeler l'une des nombreuses fonctions d'échange du contrat périphérique (laquelle dépend de l'implication ou non d'ETH, si le trader spécifie le montant de jetons à déposer ou le montant de jetons à récupérer, etc.).
Chaque fonction d'échange accepte un
path, un tableau d'échanges à parcourir.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Identifier les montants qui doivent être échangés sur chaque échange le long du chemin.
- Itère sur le chemin. Pour chaque échange en cours de route, il envoie le jeton d'entrée puis appelle la fonction
swapde l'échange. Dans la plupart des cas, l'adresse de destination des jetons est la paire d'échange suivante sur le chemin. Dans l'échange final, c'est l'adresse fournie par le trader.
Dans le contrat principal (UniswapV2Pair.sol)
- Vérifier que le contrat principal n'est pas trompé et peut maintenir une liquidité suffisante après l'échange.
- Voir combien de jetons supplémentaires nous avons en plus des réserves connues. Ce montant correspond au nombre de jetons d'entrée que nous avons reçus pour l'échange.
- Envoyer les jetons de sortie à la destination.
- Appeler
_updatepour mettre à jour les montants des réserves
De retour dans le contrat périphérique (UniswapV2Router02.sol)
- Effectuer tout nettoyage nécessaire (par exemple, brûler des jetons WETH pour récupérer de l'ETH à envoyer au trader)
Ajouter de la liquidité
Appelant
- Fournir au compte périphérique une allocation des montants à ajouter à la réserve de liquidité.
- Appeler l'une des fonctions
addLiquiditydu contrat périphérique.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Créer une nouvelle paire d'échange si nécessaire
- S'il existe une paire d'échange, calculer le montant de jetons à ajouter. Cela est censé être une valeur identique pour les deux jetons, donc le même ratio de nouveaux jetons par rapport aux 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 principal.
Dans le contrat principal (UniswapV2Pair.sol)
- Frapper des jetons de liquidité et les envoyer à l'appelant
- Appeler
_updatepour mettre à jour les montants des réserves
Retirer de la liquidité
Appelant
- Fournir au compte périphérique une allocation de jetons de liquidité à brûler en échange des jetons sous-jacents.
- Appeler l'une des fonctions
removeLiquiditydu contrat périphérique.
Dans le contrat périphérique (UniswapV2Router02.sol)
- Envoyer les jetons de liquidité à la paire d'échange
Dans le contrat principal (UniswapV2Pair.sol)
- Envoyer à l'adresse de destination les jetons sous-jacents proportionnellement aux jetons brûlés. Par exemple, s'il y a 1000 jetons A dans la réserve, 500 jetons B et 90 jetons de liquidité, et que nous recevons 9 jetons à brûler, nous brûlons 10 % des jetons de liquidité, nous renvoyons donc à l'utilisateur 100 jetons A et 50 jetons B.
- Brûler les jetons de liquidité
- Appeler
_updatepour mettre à jour les montants des réserves
Les contrats principaux
Ce sont les contrats sécurisés qui détiennent la liquidité.
UniswapV2Pair.sol
Ce contrat (opens in a new tab) implémente la réserve réelle qui échange les jetons. C'est la fonctionnalité principale d'Uniswap.
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';
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.
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
Ce contrat hérite de UniswapV2ERC20, qui fournit les fonctions ERC-20 pour les jetons de liquidité.
using SafeMath for uint;
La bibliothèque SafeMath (opens in a new tab) est utilisée pour éviter les dépassements de capacité par le haut (overflows) et par le bas (underflows). C'est important car sinon nous pourrions nous retrouver dans une situation où une valeur devrait être -1, mais est à la place 2^256-1.
using UQ112x112 for uint224;
Beaucoup de calculs dans le contrat de la réserve nécessitent des fractions. Cependant, les fractions ne sont pas prises en charge par l'EVM.
La solution trouvée par Uniswap est d'utiliser des valeurs de 224 bits, avec 112 bits pour la partie entière et 112 bits pour la fraction. Ainsi, 1.0 est représenté par 2^112, 1.5 est représenté par 2^112 + 2^111, etc.
Plus de détails sur cette bibliothèque sont disponibles plus loin dans le document.
Variables
uint public constant MINIMUM_LIQUIDITY = 10**3;
Pour éviter les cas de division par zéro, il y a un nombre minimum de jetons de liquidité qui existent toujours (mais qui appartiennent au compte zéro). Ce nombre est MINIMUM_LIQUIDITY, soit mille.
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
C'est le sélecteur d'ABI pour la fonction de transfert ERC-20. Il est utilisé pour transférer des jetons ERC-20 dans les deux comptes de jetons.
address public factory;
C'est le contrat d'usine (factory) qui a créé cette réserve. Chaque réserve est un échange entre deux jetons ERC-20, l'usine est un point central qui connecte toutes ces réserves.
address public token0;
address public token1;
Il y a les adresses des contrats pour les deux types de jetons ERC-20 qui peuvent être échangés par cette réserve.
uint112 private reserve0; // utilise un seul emplacement de stockage, accessible via getReserves
uint112 private reserve1; // utilise un seul emplacement de stockage, accessible via getReserves
Les réserves que la réserve de liquidité possède pour chaque type de jeton. Nous supposons que les deux représentent la même quantité de valeur, et par conséquent chaque token0 vaut reserve1/reserve0 token1.
uint32 private blockTimestampLast; // utilise un seul emplacement de stockage, accessible via getReserves
L'horodatage du dernier bloc dans lequel un échange a eu lieu, utilisé pour suivre les taux de change au fil du temps.
L'une des plus grandes dépenses en gaz des contrats Ethereum est le stockage, qui persiste d'un appel du contrat au suivant. Chaque cellule de stockage a une longueur de 256 bits. Ainsi, trois variables, reserve0, reserve1 et blockTimestampLast, sont allouées de telle manière qu'une seule valeur de stockage peut les inclure toutes les trois (112+112+32=256).
uint public price0CumulativeLast;
uint public price1CumulativeLast;
Ces variables contiennent les coûts cumulés pour chaque jeton (chacun en fonction de l'autre). Elles peuvent être utilisées pour calculer le taux de change moyen sur une période donnée.
uint public kLast; // reserve0 * reserve1, immédiatement après le plus récent événement de liquidité
La façon dont l'échange de paires décide du taux de change entre token0 et token1 est de maintenir le multiple des deux réserves constant pendant les transactions. kLast est cette valeur. Elle change lorsqu'un fournisseur de liquidité dépose ou retire des jetons, et elle augmente légèrement en raison des frais de marché de 0,3 %.
Voici un exemple simple. Notez que par souci de simplicité, le tableau ne comporte que trois chiffres après la virgule, et nous ignorons les frais de transaction de 0,3 %, de sorte que les chiffres ne sont pas exacts.
| Événement | reserve0 | reserve1 | reserve0 * reserve1 | Taux de change moyen (token1 / token0) |
|---|---|---|---|---|
| Configuration initiale | 1,000.000 | 1,000.000 | 1,000,000 | |
| Le trader A échange 50 token0 contre 47.619 token1 | 1,050.000 | 952.381 | 1,000,000 | 0.952 |
| Le trader B échange 10 token0 contre 8.984 token1 | 1,060.000 | 943.396 | 1,000,000 | 0.898 |
| Le trader C échange 40 token0 contre 34.305 token1 | 1,100.000 | 909.090 | 1,000,000 | 0.858 |
| Le trader D échange 100 token1 contre 109.01 token0 | 990.990 | 1,009.090 | 1,000,000 | 0.917 |
| Le trader E échange 10 token0 contre 10.079 token1 | 1,000.990 | 999.010 | 1,000,000 | 1.008 |
À mesure que les traders fournissent plus de token0, la valeur relative de token1 augmente, et vice versa, en fonction de l'offre et de la demande.
Verrou (Lock)
uint private unlocked = 1;
Il existe une classe de vulnérabilités de sécurité qui sont basées sur l'abus de réentrance (opens in a new tab). Uniswap doit transférer des jetons ERC-20 arbitraires, ce qui signifie appeler des contrats ERC-20 qui peuvent tenter d'abuser du marché Uniswap qui les appelle.
En ayant une variable unlocked dans le contrat, nous pouvons empêcher les fonctions d'être appelées pendant qu'elles sont en cours d'exécution (au sein de la même transaction).
modifier lock() {
Cette fonction est un modificateur (modifier) (opens in a new tab), une fonction qui enveloppe une fonction normale pour modifier son comportement d'une manière ou d'une autre.
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
Si unlocked est égal à un, le définir à zéro. S'il est déjà à zéro, annuler l'appel, le faire échouer.
_;
Dans un modificateur, _; est l'appel de la fonction d'origine (avec tous les paramètres). Ici, cela signifie que l'appel de la fonction ne se produit que si unlocked valait un lorsqu'elle a été appelée, et pendant son exécution, la valeur de unlocked est zéro.
unlocked = 1;
}
Une fois que la fonction principale a retourné son résultat, relâcher le verrou.
Fonctions diverses
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
Cette fonction fournit aux appelants l'état actuel de l'échange. Remarquez que les fonctions Solidity peuvent retourner plusieurs valeurs (opens in a new tab).
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
Cette fonction interne transfère une quantité de jetons ERC-20 de l'échange vers quelqu'un d'autre. SELECTOR spécifie que la fonction que nous appelons est transfer(address,uint) (voir la définition ci-dessus).
Pour éviter d'avoir à importer une interface pour la fonction du jeton, nous créons « manuellement » l'appel en utilisant l'une des fonctions de l'ABI (opens in a new tab).
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
Il y a deux façons pour un appel de transfert ERC-20 de signaler un échec :
- Annuler. Si un appel à un contrat externe est annulé, alors la valeur de retour booléenne est
false - Se terminer normalement mais signaler un échec. Dans ce cas, le tampon de la valeur de retour a une longueur non nulle, et lorsqu'il est décodé en tant que valeur booléenne, il est
false
Si l'une de ces conditions se produit, annuler.
Événements
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
Ces deux événements sont émis lorsqu'un fournisseur de liquidité dépose de la liquidité (Mint) ou la retire (Burn). Dans les deux cas, les montants de token0 et token1 qui sont déposés ou retirés font partie de l'événement, ainsi que l'identité du compte qui nous a appelés (sender). Dans le cas d'un retrait, l'événement inclut également la cible qui a reçu les jetons (to), qui peut ne pas être la même que l'expéditeur.
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
Cet événement est émis lorsqu'un trader échange un jeton contre l'autre. Encore une fois, l'expéditeur et la destination peuvent ne pas être les mêmes. Chaque jeton peut être soit envoyé à l'échange, soit reçu de celui-ci.
event Sync(uint112 reserve0, uint112 reserve1);
Enfin, Sync est émis chaque fois que des jetons sont ajoutés ou retirés, quelle qu'en soit la raison, pour fournir les dernières informations sur les réserves (et donc le taux de change).
Fonctions de configuration
Ces fonctions sont censées être appelées une fois lors de la configuration du nouvel échange de paires.
constructor() public {
factory = msg.sender;
}
Le constructeur s'assure que nous garderons une trace de l'adresse de l'usine qui a créé la paire. Cette information est requise pour initialize et pour les frais de l'usine (s'il y en a).
// appelé une fois par la fabrique au moment du déploiement
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // vérification suffisante
token0 = _token0;
token1 = _token1;
}
Cette fonction permet à l'usine (et uniquement à l'usine) de spécifier les deux jetons ERC-20 que cette paire échangera.
Fonctions de mise à jour internes
_update
// mettre à jour les réserves et, lors du premier appel par bloc, les accumulateurs de prix
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
Cette fonction est appelée chaque fois que des jetons sont déposés ou retirés.
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
Si balance0 ou balance1 (uint256) est supérieur à uint112(-1) (=2^112-1) (donc il y a un dépassement de capacité et il revient à 0 lors de la conversion en uint112), refuser de continuer la fonction _update pour éviter les dépassements de capacité. Avec un jeton normal qui peut être subdivisé en 10^18 unités, cela signifie que chaque échange est limité à environ 5,1*10^15 de chaque jeton. Jusqu'à présent, cela n'a pas posé de problème.
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // le débordement est souhaité
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
Si le temps écoulé n'est pas nul, cela signifie que nous sommes la première transaction d'échange sur ce bloc. Dans ce cas, nous devons mettre à jour les accumulateurs de coûts.
// * ne déborde jamais, et le débordement de + est souhaité
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
Chaque accumulateur de coût est mis à jour avec le dernier coût (réserve de l'autre jeton/réserve de ce jeton) multiplié par le temps écoulé en secondes. Pour obtenir un prix moyen, vous lisez le prix cumulé à deux moments différents et vous divisez par la différence de temps entre eux. Par exemple, supposons cette séquence d'événements :
| Événement | reserve0 | reserve1 | horodatage | Taux de change marginal (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Configuration initiale | 1,000.000 | 1,000.000 | 5,000 | 1.000 | 0 |
| Le trader A dépose 50 token0 et récupère 47.619 token1 | 1,050.000 | 952.381 | 5,020 | 0.907 | 20 |
| Le trader B dépose 10 token0 et récupère 8.984 token1 | 1,060.000 | 943.396 | 5,030 | 0.890 | 20+10*0.907 = 29.07 |
| Le trader C dépose 40 token0 et récupère 34.305 token1 | 1,100.000 | 909.090 | 5,100 | 0.826 | 29.07+70*0.890 = 91.37 |
| Le trader D dépose 100 token1 et récupère 109.01 token0 | 990.990 | 1,009.090 | 5,110 | 1.018 | 91.37+10*0.826 = 99.63 |
| Le trader E dépose 10 token0 et récupère 10.079 token1 | 1,000.990 | 999.010 | 5,150 | 0.998 | 99.63+40*1.1018 = 143.702 |
Disons que nous voulons calculer le prix moyen de Token0 entre les horodatages 5,030 et 5,150. La différence de la valeur de price0Cumulative est 143.702-29.07=114.632. C'est la moyenne sur deux minutes (120 secondes). Le prix moyen est donc de 114.632/120 = 0.955.
Ce calcul de prix est la raison pour laquelle nous devons connaître les anciennes tailles des réserves.
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
Enfin, mettre à jour les variables globales et émettre un événement Sync.
_mintFee
// si les frais sont activés, frapper une liquidité équivalente à 1/6 de la croissance de sqrt(k)
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
Dans Uniswap 2.0, les traders paient des frais de 0,30 % pour utiliser le marché. La majeure partie de ces frais (0,25 % de la transaction) revient toujours aux fournisseurs de liquidité. Les 0,05 % restants peuvent aller soit aux fournisseurs de liquidité, soit à une adresse spécifiée par l'usine en tant que frais de protocole, ce qui rémunère Uniswap pour ses efforts de développement.
Pour réduire les calculs (et donc les coûts en gaz), ces frais ne sont calculés que lorsque de la liquidité est ajoutée ou retirée de la réserve de liquidité, plutôt qu'à chaque transaction.
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
Lire la destination des frais de l'usine. Si elle est de zéro, alors il n'y a pas de frais de protocole et il n'est pas nécessaire de calculer ces frais.
uint _kLast = kLast; // économies de gaz
La variable d'état kLast est située dans le stockage, elle aura donc une valeur entre les différents appels au contrat.
L'accès au stockage est beaucoup plus coûteux que l'accès à la mémoire volatile qui est libérée lorsque l'appel de la fonction au contrat se termine, nous utilisons donc une variable interne pour économiser du gaz.
if (feeOn) {
if (_kLast != 0) {
Les fournisseurs de liquidité obtiennent leur part simplement par l'appréciation de leurs jetons de liquidité. Mais les frais de protocole nécessitent que de nouveaux jetons de liquidité soient frappés et fournis à l'adresse feeTo.
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
S'il y a une nouvelle liquidité sur laquelle percevoir des frais de protocole. Vous pouvez voir la fonction de racine carrée plus loin dans cet article
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
uint liquidity = numerator / denominator;
Ce calcul compliqué des frais est expliqué dans le livre blanc (opens in a new tab) à la page 5. Nous savons qu'entre le moment où kLast a été calculé et le présent, aucune liquidité n'a été ajoutée ou retirée (car nous exécutons ce calcul chaque fois que de la liquidité est ajoutée ou retirée, avant qu'elle ne change réellement), donc tout changement dans reserve0 * reserve1 doit provenir des frais de transaction (sans eux, nous garderions reserve0 * reserve1 constant).
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
Utiliser la fonction UniswapV2ERC20._mint pour créer réellement les jetons de liquidité supplémentaires et les attribuer à feeTo.
} else if (_kLast != 0) {
kLast = 0;
}
}
S'il n'y a pas de frais, définir kLast à zéro (si ce n'est pas déjà le cas). Lorsque ce contrat a été écrit, il y avait 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 remettant à zéro le stockage dont ils n'avaient pas besoin.
Ce code obtient ce remboursement lorsque cela est possible.
Fonctions accessibles de l'extérieur
Notez que bien que n'importe quelle transaction ou contrat puisse appeler ces fonctions, elles sont conçues pour être appelées depuis le contrat périphérique. Si vous les appelez directement, vous ne pourrez pas tromper l'échange de paires, mais vous pourriez perdre de la valeur à cause d'une erreur.
mint
// cette fonction de bas niveau doit être appelée depuis un contrat qui effectue des vérifications de sécurité importantes
function mint(address to) external lock returns (uint liquidity) {
Cette fonction est appelée lorsqu'un fournisseur de liquidité ajoute de la liquidité à la réserve de liquidité. Elle frappe des jetons de liquidité supplémentaires en guise de récompense. Elle doit être appelée depuis un contrat périphérique qui l'appelle après avoir ajouté la liquidité dans la même transaction (afin que personne d'autre ne puisse soumettre une transaction qui réclame la nouvelle liquidité avant le propriétaire légitime).
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // économies de gaz
C'est la façon de lire les résultats d'une fonction Solidity qui retourne plusieurs valeurs. Nous ignorons la dernière valeur retournée, l'horodatage du bloc, car nous n'en avons pas besoin.
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
Obtenir les soldes actuels et voir combien a été ajouté pour chaque type de jeton.
bool feeOn = _mintFee(_reserve0, _reserve1);
Calculer les frais de protocole à percevoir, le cas échéant, et frapper les jetons de liquidité en conséquence. Étant donné que les paramètres de _mintFee sont les anciennes valeurs de réserve, les frais sont calculés avec précision en se basant uniquement sur les changements de la réserve dus aux frais.
uint _totalSupply = totalSupply; // économies de gaz, doit être défini ici car totalSupply peut se mettre à jour dans _mintFee
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // verrouiller de façon permanente les premiers jetons MINIMUM_LIQUIDITY
S'il s'agit du premier dépôt, créer MINIMUM_LIQUIDITY jetons et les envoyer à l'adresse zéro pour les verrouiller. Ils ne pourront jamais être rachetés, ce qui signifie que la réserve ne sera jamais complètement vidée (cela nous évite des divisions par zéro à certains endroits). La valeur de MINIMUM_LIQUIDITY est de mille, ce qui, considérant que la plupart des ERC-20 sont subdivisés en unités de 10^-18 d'un jeton, tout comme l'ETH est divisé en Wei, représente 10^-15 de la valeur d'un seul jeton. Ce n'est pas un coût élevé.
Au moment du premier dépôt, nous ne connaissons pas la valeur relative des deux jetons, nous multiplions donc simplement les montants et prenons une racine carrée, en supposant que le dépôt nous fournit une valeur égale dans les deux jetons.
Nous pouvons nous y fier car il est dans l'intérêt du déposant de fournir une valeur égale, pour éviter de perdre de la valeur à cause de l'arbitrage. Disons que la valeur des deux jetons est identique, mais que notre déposant a déposé quatre fois plus de Token1 que de Token0. Un trader peut utiliser le fait que l'échange de paires pense que Token0 a plus de valeur pour en extraire de la valeur.
| Événement | reserve0 | reserve1 | reserve0 * reserve1 | Valeur de la réserve (reserve0 + reserve1) |
|---|---|---|---|---|
| Configuration initiale | 8 | 32 | 256 | 40 |
| Le trader dépose 8 jetons Token0, récupère 16 Token1 | 16 | 16 | 256 | 32 |
Comme vous pouvez le voir, le trader a gagné 8 jetons supplémentaires, qui proviennent d'une réduction de la valeur de la réserve, ce qui nuit au déposant qui la possède.
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
Avec chaque dépôt ultérieur, nous connaissons déjà le taux de change entre les deux actifs, et nous nous attendons à ce que les fournisseurs de liquidité fournissent une valeur égale dans les deux. S'ils ne le font pas, nous leur donnons des jetons de liquidité basés sur la valeur la plus faible qu'ils ont fournie, en guise de punition.
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 de reserve0*reserve1 et la valeur du jeton de liquidité ne change pas (à moins que nous ne recevions un dépôt qui n'a pas des valeurs égales des 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 | reserve0 | reserve1 | reserve0 * reserve1 | Valeur de la réserve (reserve0 + reserve1) | Jetons de liquidité frappés pour ce dépôt | Total des jetons de liquidité | Valeur de chaque jeton de liquidité |
|---|---|---|---|---|---|---|---|
| Configuration initiale | 8.000 | 8.000 | 64 | 16.000 | 8 | 8 | 2.000 |
| Dépôt de quatre de chaque type | 12.000 | 12.000 | 144 | 24.000 | 4 | 12 | 2.000 |
| Dépôt de deux de chaque type | 14.000 | 14.000 | 196 | 28.000 | 2 | 14 | 2.000 |
| Dépôt de valeur inégale | 18.000 | 14.000 | 252 | 32.000 | 0 | 14 | ~2.286 |
| Après arbitrage | ~15.874 | ~15.874 | 252 | ~31.748 | 0 | 14 | ~2.267 |
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
Utiliser la fonction UniswapV2ERC20._mint pour créer réellement les jetons de liquidité supplémentaires et les donner au bon compte.
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 et reserve1 sont à jour
emit Mint(msg.sender, amount0, amount1);
}
Mettre à jour les variables d'état (reserve0, reserve1, et si nécessaire kLast) et émettre l'événement approprié.
burn
// cette fonction de bas niveau doit être appelée depuis un contrat qui effectue des vérifications de sécurité importantes
function burn(address to) external lock returns (uint amount0, uint amount1) {
Cette fonction est appelée lorsque de la liquidité est retirée et que les jetons de liquidité appropriés doivent être brûlés. Elle doit également être appelée depuis un compte périphérique.
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // économies de gaz
address _token0 = token0; // économies de gaz
address _token1 = token1; // économies de gaz
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
Le contrat périphérique a transféré la liquidité à brûler à ce contrat avant l'appel. De cette façon, nous savons quelle quantité de liquidité brûler, et nous pouvons nous assurer qu'elle est bien brûlée.
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // économies de gaz, doit être défini ici car totalSupply peut se mettre à jour dans _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // l'utilisation des soldes garantit une distribution au prorata
amount1 = liquidity.mul(balance1) / _totalSupply; // l'utilisation des soldes garantit une distribution au prorata
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
Le fournisseur de liquidité reçoit une valeur égale des deux jetons. De cette façon, nous ne modifions pas le taux de change.
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 et reserve1 sont à jour
emit Burn(msg.sender, amount0, amount1, to);
}
Le reste de la fonction burn est l'image miroir de la fonction mint ci-dessus.
swap
// cette fonction de bas niveau doit être appelée depuis un contrat qui effectue des vérifications de sécurité importantes
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
Cette fonction est également censée être appelée depuis un contrat périphérique.
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // économies de gaz
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // portée pour _token{0,1}, évite les erreurs de pile trop profonde
Les variables locales peuvent être stockées soit en mémoire, soit, s'il n'y en a pas trop, directement sur la pile. Si nous pouvons limiter le nombre pour utiliser la pile, nous utilisons moins de gaz. Pour plus de détails, voir le livre jaune, les spécifications formelles d'Ethereum (opens in a new tab), p. 26, équation 298.
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // transférer les jetons de manière optimiste
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // transférer les jetons de manière optimiste
Ce transfert est optimiste, car nous transférons avant d'être sûrs que toutes les conditions sont remplies. C'est acceptable dans Ethereum car si les conditions ne sont pas remplies plus tard dans l'appel, nous l'annulons ainsi que toutes les modifications qu'il a créées.
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
Informer le destinataire de l'échange si demandé.
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
Obtenir les soldes actuels. Le contrat périphérique nous envoie les jetons avant de nous appeler pour l'échange. Cela permet au contrat de vérifier facilement qu'il n'est pas trompé, une vérification qui doit avoir lieu dans le contrat principal (car nous pouvons être appelés par d'autres entités que notre contrat périphérique).
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // portée pour reserve{0,1}Adjusted, évite les erreurs de pile trop profonde
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
C'est une vérification de cohérence pour s'assurer que nous ne perdons pas lors de l'échange. Il n'y a aucune circonstance dans laquelle un échange devrait réduire reserve0*reserve1. C'est également ici que nous nous assurons qu'une commission de 0,3 % est envoyée sur l'échange ; avant de vérifier la cohérence de la valeur de K, nous multiplions les deux soldes par 1000 soustrait des montants multipliés par 3, cela signifie que 0,3 % (3/1000 = 0,003 = 0,3 %) est déduit du solde avant de comparer sa valeur K avec la valeur K des réserves actuelles.
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
Mettre à jour reserve0 et reserve1, et si nécessaire les accumulateurs de prix et l'horodatage, et émettre un événement.
Sync ou Skim
Il est possible que les soldes réels se désynchronisent des réserves que l'échange de paires pense avoir.
Il n'y a aucun moyen de retirer des jetons sans le consentement du contrat, mais les dépôts sont une autre affaire. Un compte peut transférer des jetons à l'échange sans appeler ni mint ni swap.
Dans ce cas, il y a deux solutions :
sync, mettre à jour les réserves avec les soldes actuelsskim, retirer le montant supplémentaire. Notez que n'importe quel compte est autorisé à appelerskimcar nous ne savons pas qui a déposé les jetons. Cette information est émise dans un événement, mais les événements ne sont pas accessibles depuis la chaîne de blocs.
// forcer les soldes à correspondre aux réserves
function skim(address to) external lock {
address _token0 = token0; // économies de gaz
address _token1 = token1; // économies de gaz
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
// forcer les réserves à correspondre aux soldes
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
}
UniswapV2Factory.sol
Ce contrat (opens in a new tab) crée les échanges de paires.
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
contract UniswapV2Factory is IUniswapV2Factory {
address public feeTo;
address public feeToSetter;
Ces variables d'état sont nécessaires pour implémenter les frais de protocole (voir le livre blanc (opens in a new tab), p. 5).
L'adresse feeTo accumule les jetons de liquidité pour les frais de protocole, et feeToSetter est l'adresse autorisée à changer feeTo pour une adresse différente.
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
Ces variables gardent une trace des paires, les échanges entre deux types de jetons.
La première, getPair, est un mapping qui identifie un contrat d'échange de paires en fonction des deux jetons ERC-20 qu'il échange. Les jetons ERC-20 sont identifiés par les adresses des contrats qui les implémentent, donc les clés et la valeur sont toutes des adresses. Pour obtenir l'adresse de l'échange de paires qui vous permet de convertir de tokenA à tokenB, vous utilisez getPair[<tokenA address>][<tokenB address>] (ou l'inverse).
La deuxième variable, allPairs, est un tableau qui inclut toutes les adresses des échanges de paires créés par cette usine. Dans Ethereum, vous ne pouvez pas itérer sur le contenu d'un mapping, ni obtenir une liste de toutes les clés, donc cette variable est le seul moyen de savoir quels échanges cette usine gère.
Remarque : La raison pour laquelle vous ne pouvez pas itérer sur toutes les clés d'un mapping est que le stockage des données du contrat est coûteux, donc moins nous en utilisons, mieux c'est, et moins souvent nous le modifions, mieux c'est. Vous pouvez créer des mappings qui prennent en charge l'itération (opens in a new tab), mais ils nécessitent un stockage supplémentaire pour une liste de clés. Dans la plupart des applications, vous n'en avez pas besoin.
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
Cet événement est émis lorsqu'un nouvel échange de paires est créé. Il inclut les adresses des jetons, l'adresse de l'échange de paires et le nombre total d'échanges gérés par l'usine.
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
La seule chose que fait le constructeur est de spécifier le feeToSetter. Les usines démarrent sans frais, et seul feeSetter peut changer cela.
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
Cette fonction retourne le nombre de paires d'échange.
function createPair(address tokenA, address tokenB) external returns (address pair) {
C'est la fonction principale de l'usine, pour 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 de la permission d'Uniswap pour créer un nouvel échange de paires.
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
Nous voulons que l'adresse du nouvel échange soit déterministe, afin qu'elle puisse être calculée à l'avance hors chaîne (cela peut être utile pour les transactions de couche 2 (l2)). Pour ce faire, nous devons avoir un ordre cohérent des adresses de jetons, quel que soit l'ordre dans lequel nous les avons reçues, nous les trions donc ici.
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // une seule vérification est suffisante
Les grandes réserves de liquidité sont meilleures que les petites, car elles ont des prix plus stables. Nous ne voulons pas avoir plus d'une seule réserve de liquidité par paire de jetons. S'il y a déjà un échange, il n'est pas nécessaire d'en créer un autre pour la même paire.
bytes memory bytecode = type(UniswapV2Pair).creationCode;
Pour créer un nouveau contrat, nous avons besoin du code qui le crée (à la fois la fonction constructeur et le code qui écrit en mémoire le bytecode EVM du contrat réel). Normalement, dans Solidity, nous utilisons simplement addr = new <name of contract>(<constructor parameters>) et le compilateur s'occupe de tout pour nous, mais pour avoir une adresse de contrat déterministe, nous devons utiliser le code d'opération CREATE2 (opens in a new tab).
Lorsque ce code a été écrit, ce code d'opération n'était pas encore pris en charge par Solidity, il était donc nécessaire d'obtenir manuellement le code. Ce n'est plus un problème, car Solidity prend désormais en charge CREATE2 (opens in a new tab).
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
Lorsqu'un code d'opération n'est pas encore pris en charge par Solidity, nous pouvons l'appeler en utilisant l'assembleur en ligne (inline assembly) (opens in a new tab).
IUniswapV2Pair(pair).initialize(token0, token1);
Appeler la fonction initialize pour indiquer au nouvel échange quels sont les deux jetons qu'il échange.
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // remplir le mapping dans le sens inverse
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
Enregistrer les informations de la nouvelle paire dans les variables d'état et émettre un événement pour informer le monde du nouvel échange de paires.
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}
Ces deux fonctions permettent à feeSetter de contrôler le destinataire des frais (le cas échéant), et de changer feeSetter pour une nouvelle adresse.
UniswapV2ERC20.sol
Ce contrat (opens in a new tab) implémente le jeton de liquidité ERC-20. Il est similaire au contrat ERC-20 d'OpenZeppelin, je n'expliquerai donc que la partie qui est différente, la fonctionnalité permit.
Les transactions sur Ethereum coûtent de l'ether (ETH), ce qui équivaut à de l'argent réel. Si vous avez des jetons ERC-20 mais pas d'ETH, vous ne pouvez pas envoyer de transactions, vous ne pouvez donc rien faire avec. Une solution pour éviter ce problème est les 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 l'envoie via Internet au destinataire. Le destinataire, qui possède de l'ETH, soumet ensuite le permis au nom du propriétaire.
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
Ce hash est l'identifiant du type de transaction (opens in a new tab). Le seul que nous prenons en charge ici est Permit avec ces paramètres.
mapping(address => uint) public nonces;
Il n'est pas possible pour un destinataire de falsifier une signature numérique. Cependant, il est trivial d'envoyer la même transaction deux fois (c'est une forme d'attaque par rejeu (opens in a new tab)). Pour éviter cela, nous utilisons un nonce (opens in a new tab). Si le nonce d'un nouveau Permit n'est pas supérieur de un au dernier utilisé, nous supposons qu'il est invalide.
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
C'est le code pour récupérer l'identifiant de la chaîne (opens in a new tab). Il utilise un dialecte d'assembleur EVM appelé Yul (opens in a new tab). Notez que dans la version actuelle de Yul, vous devez utiliser chainid(), et non chainid.
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}
Calculer le séparateur de domaine (opens in a new tab) pour l'EIP-712.
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
C'est la fonction qui implémente les permissions. Elle reçoit en paramètres les champs pertinents, et les trois valeurs scalaires pour la signature (opens in a new tab) (v, r et s).
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
Ne pas accepter les transactions après la date limite.
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
abi.encodePacked(...) est le message que nous nous attendons à recevoir. Nous savons quel devrait être le nonce, il n'est donc pas nécessaire de l'obtenir en tant que paramètre.
L'algorithme de signature d'Ethereum s'attend à recevoir 256 bits à signer, nous utilisons donc la fonction de hachage keccak256.
address recoveredAddress = ecrecover(digest, v, r, s);
À partir du condensé (digest) et de la signature, nous pouvons obtenir l'adresse qui l'a signé en utilisant ecrecover (opens in a new tab).
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
Si tout est correct, traiter cela comme un approuver (approve) ERC-20 (opens in a new tab).
Les contrats périphériques
Les contrats périphériques constituent l'API (interface de programmation d'application) d'Uniswap. Ils sont disponibles pour des appels externes, que ce soit depuis d'autres contrats ou des applications décentralisées. Vous pourriez appeler les contrats principaux directement, mais c'est plus compliqué et vous pourriez perdre de la valeur si vous faites une erreur. Les contrats principaux ne contiennent que des tests pour s'assurer qu'ils ne sont pas trompés, et non des vérifications de cohérence pour qui que ce soit d'autre. Celles-ci se trouvent dans la périphérie afin de pouvoir être mises à jour selon les besoins.
UniswapV2Router01.sol
Ce contrat (opens in a new tab) présente des problèmes et ne devrait plus être utilisé (opens in a new tab). Heureusement, les contrats périphériques sont sans état et ne détiennent aucun actif, il est donc facile de le déprécier et de suggérer aux gens d'utiliser son remplaçant, UniswapV2Router02, à la place.
UniswapV2Router02.sol
Dans la plupart des cas, vous utiliseriez Uniswap via ce contrat (opens in a new tab). Vous pouvez voir comment l'utiliser ici (opens in a new tab).
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';
import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';
La plupart d'entre eux ont déjà été rencontrés ou sont assez évidents. La seule exception est IWETH.sol. Uniswap v2 permet des échanges pour n'importe quelle paire de jetons ERC-20, mais l'ether (ETH) lui-même n'est pas un jeton ERC-20. Il est antérieur à la norme et est transféré par des mécanismes uniques. Pour permettre l'utilisation de l'ETH dans les contrats qui s'appliquent aux jetons ERC-20, on a créé le contrat d'ether enveloppé (WETH) (opens in a new tab). Vous envoyez de l'ETH à ce contrat, et il vous frappe un montant équivalent de WETH. Ou vous pouvez brûler du WETH et récupérer de l'ETH.
contract UniswapV2Router02 is IUniswapV2Router02 {
using SafeMath for uint;
address public immutable override factory;
address public immutable override WETH;
Le routeur a besoin de savoir quelle usine utiliser, et pour les transactions qui nécessitent du WETH, quel contrat WETH utiliser. Ces valeurs sont immuables (opens in a new tab), ce qui signifie qu'elles ne peuvent être définies que dans le constructeur. Cela donne aux utilisateurs l'assurance que personne ne pourra les modifier pour pointer vers des contrats moins honnêtes.
modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}
Ce modificateur s'assure que les transactions limitées dans le temps (« faire X avant le temps Y si possible ») ne se produisent pas après leur limite de temps.
constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}
Le constructeur se contente de définir les variables d'état immuables.
receive() external payable {
assert(msg.sender == WETH); // n'accepter de l'ETH via fallback que depuis le contrat WETH
}
Cette fonction est appelée lorsque nous rachetons des jetons du contrat WETH pour les reconvertir en ETH. Seul le contrat WETH que nous utilisons est autorisé à le faire.
Ajouter de la liquidité
Ces fonctions ajoutent des jetons à l'échange de paires, ce qui augmente la réserve de liquidité.
// **** AJOUTER DE LA LIQUIDITÉ ****
function _addLiquidity(
Cette fonction est utilisée pour calculer le montant de jetons A et B qui doit être déposé dans l'échange de paires.
address tokenA,
address tokenB,
Ce sont les adresses des contrats de jetons ERC-20.
uint amountADesired,
uint amountBDesired,
Ce sont les montants que le fournisseur de liquidité souhaite déposer. Ce sont également les montants maximums de A et B à déposer.
uint amountAMin,
uint amountBMin
Ce sont les montants minimums acceptables à déposer. Si la transaction ne peut pas avoir lieu avec ces montants ou plus, il faut l'annuler. Si vous ne voulez pas de cette fonctionnalité, spécifiez simplement zéro.
Les fournisseurs de liquidité spécifient généralement un minimum, car ils veulent limiter la transaction à un taux de change proche du taux actuel. Si le taux de change fluctue trop, cela pourrait signifier que des nouvelles modifient les valeurs sous-jacentes, et ils veulent décider manuellement de ce qu'il faut faire.
Par exemple, imaginez un cas où le taux de change est de un pour un et où le fournisseur de liquidité spécifie ces valeurs :
| Paramètre | Valeur |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
Tant que le taux de change reste entre 0,9 et 1,25, la transaction a lieu. Si le taux de change sort de cette fourchette, la transaction est annulée.
La raison de cette précaution est que les transactions ne sont pas immédiates, vous les soumettez et finalement un validateur va les inclure dans un bloc (à moins que votre prix du gaz ne soit très bas, auquel cas vous devrez soumettre une autre transaction avec le même nonce et un prix du gaz plus élevé pour l'écraser). Vous ne pouvez pas contrôler ce qui se passe pendant l'intervalle entre la soumission et l'inclusion.
) internal virtual returns (uint amountA, uint amountB) {
La fonction renvoie les montants que le fournisseur de liquidité devrait déposer pour avoir un ratio égal au ratio actuel entre les réserves.
// créer la paire si elle n'existe pas encore
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
S'il n'y a pas encore d'échange pour cette paire de jetons, créez-le.
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
Obtenir les réserves actuelles dans la paire.
if (reserveA == 0 && reserveB == 0) {
(amountA, amountB) = (amountADesired, amountBDesired);
Si les réserves actuelles sont vides, il s'agit d'un nouvel échange de paires. Les montants à déposer doivent être exactement les mêmes que ceux que le fournisseur de liquidité souhaite fournir.
} else {
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
Si nous avons besoin de voir quels seront les montants, nous obtenons le montant optimal en utilisant cette fonction (opens in a new tab). Nous voulons le même ratio que les réserves actuelles.
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
Si amountBOptimal est inférieur au montant que le fournisseur de liquidité souhaite déposer, cela signifie que le jeton B a actuellement plus de valeur que ne le pense le déposant de liquidité, un montant inférieur est donc requis.
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
Si le montant optimal de B est supérieur au montant souhaité de B, cela signifie que les jetons B ont actuellement moins de valeur que ne le pense le déposant de liquidité, un montant plus élevé est donc requis. Cependant, le montant souhaité est un maximum, nous ne pouvons donc pas faire cela. À la place, nous calculons le nombre optimal de jetons A pour le montant souhaité de jetons B.
En rassemblant tout cela, nous obtenons ce graphique. Supposons que vous essayiez de déposer mille jetons A (ligne bleue) et mille jetons B (ligne rouge). L'axe des x est le taux de change, A/B. Si x=1, ils ont une valeur égale et vous déposez mille de chaque. Si x=2, A vaut le double de B (vous obtenez deux jetons B pour chaque jeton A), vous déposez donc mille jetons B, mais seulement 500 jetons A. Si x=0,5, la situation est inversée, mille jetons A et cinq cents jetons B.
Vous pourriez déposer de la liquidité directement dans le contrat principal (en utilisant UniswapV2Pair::mint (opens in a new tab)), mais le contrat principal vérifie seulement qu'il n'est pas lui-même trompé, vous courez donc le risque de perdre de la valeur si le taux de change change entre le moment où vous soumettez votre transaction et le moment où elle est exécutée. Si vous utilisez le contrat périphérique, il calcule le montant que vous devriez déposer et le dépose immédiatement, de sorte que le taux de change ne change pas et que vous ne perdez rien.
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
Cette fonction peut être appelée par une transaction pour déposer de la liquidité. La plupart des paramètres sont les mêmes que dans _addLiquidity ci-dessus, à deux exceptions près :
. to est l'adresse qui reçoit les nouveaux jetons de liquidité frappés pour montrer la part du fournisseur de liquidité dans la réserve
. deadline est une limite de temps pour la transaction
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
Nous calculons les montants à déposer réellement, puis nous trouvons l'adresse de la réserve de liquidité. Pour économiser du gaz, nous ne le faisons pas en demandant à l'usine, mais en utilisant la fonction de bibliothèque pairFor (voir ci-dessous dans les bibliothèques)
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
Transférer les montants corrects de jetons de l'utilisateur vers l'échange de paires.
liquidity = IUniswapV2Pair(pair).mint(to);
}
En retour, donner à l'adresse to des jetons de liquidité pour la propriété partielle de la réserve. La fonction mint du contrat principal voit combien de jetons supplémentaires il possède (par rapport à ce qu'il avait la dernière fois que la liquidité a changé) et frappe de la liquidité en conséquence.
function addLiquidityETH(
address token,
uint amountTokenDesired,
Lorsqu'un fournisseur de liquidité souhaite fournir de la liquidité à un échange de paires Jeton/ETH, il y a quelques différences. Le contrat se charge d'envelopper l'ETH pour le fournisseur de liquidité. Il n'est pas nécessaire de spécifier combien d'ETH l'utilisateur souhaite déposer, car l'utilisateur les envoie simplement avec la transaction (le montant est disponible dans msg.value).
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
address pair = UniswapV2Library.pairFor(factory, token, WETH);
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
IWETH(WETH).deposit{value: amountETH}();
assert(IWETH(WETH).transfer(pair, amountETH));
Pour déposer l'ETH, le contrat l'enveloppe d'abord en WETH, puis transfère le WETH dans la paire. Remarquez que le transfert est enveloppé dans un assert. Cela signifie que si le transfert échoue, cet appel de contrat échoue également, et par conséquent l'enveloppement ne se produit pas vraiment.
liquidity = IUniswapV2Pair(pair).mint(to);
// rembourser la poussière d'ether, le cas échéant
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
L'utilisateur nous a déjà envoyé l'ETH, donc s'il en reste un excédent (parce que l'autre jeton a moins de valeur que ce que l'utilisateur pensait), nous devons émettre un remboursement.
Retirer de la liquidité
Ces fonctions retireront de la liquidité et rembourseront le fournisseur de liquidité.
// **** RETIRER DE LA LIQUIDITÉ ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
Le cas le plus simple de retrait de liquidité. Il y a un montant minimum de chaque jeton que le fournisseur de liquidité accepte de recevoir, et cela doit se produire avant la date limite.
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // envoyer la liquidité à la paire
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
La fonction burn du contrat principal se charge de rembourser les jetons à l'utilisateur.
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
Lorsqu'une fonction renvoie plusieurs valeurs, mais que nous ne sommes intéressés que par certaines d'entre elles, c'est ainsi que nous n'obtenons que ces valeurs. C'est un peu moins cher en termes de gaz que de lire une valeur et de ne jamais l'utiliser.
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
Traduire les montants de la façon dont le contrat principal les renvoie (le jeton avec l'adresse la plus basse en premier) à la façon dont l'utilisateur les attend (correspondant à tokenA et tokenB).
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
Il est acceptable de faire le transfert d'abord, puis de vérifier s'il est légitime, car s'il ne l'est pas, nous annulerons tous les changements d'état.
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, amountToken);
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
Le retrait de liquidité pour l'ETH est presque le même, sauf que nous recevons les jetons WETH puis les rachetons contre de l'ETH pour les rendre au fournisseur de liquidité.
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
function removeLiquidityETHWithPermit(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountToken, uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
}
Ces fonctions relaient des méta-transactions pour permettre aux utilisateurs sans ether de se retirer de la réserve, en utilisant le mécanisme de permis (permit).
// **** RETIRER DE LA LIQUIDITÉ (prenant en charge les jetons avec frais lors du transfert) ****
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountETH) {
(, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
IWETH(WETH).withdraw(amountETH);
TransferHelper.safeTransferETH(to, amountETH);
}
Cette fonction peut être utilisée pour les jetons qui ont des frais de transfert ou de stockage. Lorsqu'un jeton a de tels frais, nous ne pouvons pas compter sur la fonction removeLiquidity pour nous dire quelle quantité du jeton nous récupérons, nous devons donc d'abord retirer puis obtenir le solde.
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountETH) {
address pair = UniswapV2Library.pairFor(factory, token, WETH);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
token, liquidity, amountTokenMin, amountETHMin, to, deadline
);
}
La fonction finale combine les frais de stockage avec les méta-transactions.
Échanger
// **** ÉCHANGE ****
// exige que le montant initial ait déjà été envoyé à la première paire
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
Cette fonction effectue le traitement interne requis pour les fonctions qui sont exposées aux traders.
for (uint i; i < path.length - 1; i++) {
Au moment où j'écris 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, pour le moment, ne compte que 0,1 % de ce nombre de comptes (opens in a new tab). Au lieu de cela, les fonctions d'échange prennent en charge le concept de chemin (path). Un trader peut échanger A contre B, B contre C, et C contre D, il n'y a donc pas besoin d'un échange direct de la paire A-D.
Les prix sur ces marchés ont tendance à être synchronisés, car lorsqu'ils sont désynchronisés, cela crée une opportunité d'arbitrage. Imaginez, par exemple, trois jetons, A, B et C. Il y a trois échanges de paires, un pour chaque paire.
- La situation initiale
- Un trader vend 24,695 jetons A et obtient 25,305 jetons B.
- Le trader vend 24,695 jetons B pour 25,305 jetons C, conservant environ 0,61 jeton B comme profit.
- Ensuite, le trader vend 24,695 jetons C pour 25,305 jetons A, conservant environ 0,61 jeton C comme profit. Le trader a également 0,61 jeton A supplémentaire (les 25,305 avec lesquels le trader se retrouve, moins l'investissement initial de 24,695).
| Étape | Échange A-B | Échange B-C | Échange A-C |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
| 4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1024.695 C:1024.695 C/A=1 |
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
uint amountOut = amounts[i + 1];
Obtenir la paire que nous traitons actuellement, la trier (pour l'utiliser avec la paire) et obtenir le montant de sortie attendu.
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
Obtenir les montants de sortie attendus, triés de la façon dont l'échange de paires s'attend à ce qu'ils soient.
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
Est-ce le dernier échange ? Si oui, envoyer les jetons reçus pour la transaction à la destination. Sinon, les envoyer au prochain échange de paires.
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
Appeler réellement l'échange de paires pour échanger les jetons. Nous n'avons pas besoin d'un rappel (callback) pour être informés de l'échange, nous n'envoyons donc aucun octet dans ce champ.
function swapExactTokensForTokens(
Cette fonction est utilisée directement par les traders pour échanger un jeton contre un autre.
uint amountIn,
uint amountOutMin,
address[] calldata path,
Ce paramètre contient les adresses des contrats ERC-20. Comme expliqué ci-dessus, il s'agit d'un tableau car vous pourriez avoir besoin de passer par plusieurs échanges de paires pour passer de l'actif que vous avez à l'actif que vous voulez.
Un paramètre de fonction dans Solidity peut être stocké soit dans memory soit dans les calldata. Si la fonction est un point d'entrée du contrat, appelée directement par un utilisateur (à l'aide d'une transaction) ou par un contrat différent, alors la valeur du paramètre peut être prise directement à partir des données d'appel. Si la fonction est appelée en interne, comme _swap ci-dessus, alors les paramètres doivent être stockés dans memory. Du point de vue du contrat appelé, les calldata sont en lecture seule.
Avec des types scalaires tels que uint ou address, le compilateur gère le choix du stockage pour nous, mais avec les tableaux, qui sont plus longs et plus coûteux, nous spécifions le type de stockage à utiliser.
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
Les valeurs de retour sont toujours renvoyées en mémoire.
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
Calculer le montant à acheter dans chaque échange. Si le résultat est inférieur au minimum que le trader est prêt à accepter, annuler la transaction.
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
Enfin, transférer le jeton ERC-20 initial vers le compte pour le premier échange de paires et appeler _swap. Tout cela se passe dans la même transaction, de sorte que l'échange de paires sait que tout jeton inattendu fait partie de ce transfert.
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
La fonction précédente, swapTokensForTokens, permet à un trader de spécifier un 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 effectue l'échange inverse, elle permet à un trader de spécifier le nombre de jetons de sortie qu'il souhaite, et le nombre maximum de jetons d'entrée qu'il est prêt à payer pour les obtenir.
Dans les deux cas, le trader doit d'abord donner à ce contrat périphérique une allocation pour lui permettre de les transférer.
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
}
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
virtual
override
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, address(this));
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
external
virtual
override
payable
ensure(deadline)
returns (uint[] memory amounts)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
IWETH(WETH).deposit{value: amounts[0]}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
_swap(amounts, path, to);
// rembourser la poussière d'ether, le cas échéant
if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
}
Ces quatre variantes impliquent toutes des échanges entre l'ETH et des jetons. La seule différence est que soit nous recevons de l'ETH du trader et l'utilisons pour frapper du WETH, soit nous recevons du WETH du dernier échange dans le chemin et le brûlons, en renvoyant au trader l'ETH résultant.
// **** ÉCHANGE (prenant en charge les jetons avec frais lors du transfert) ****
// exige que le montant initial ait déjà été envoyé à la première paire
function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
Il s'agit de la fonction interne pour échanger des jetons qui ont des frais de transfert ou de stockage afin de résoudre (ce problème (opens in a new tab)).
for (uint i; i < path.length - 1; i++) {
(address input, address output) = (path[i], path[i + 1]);
(address token0,) = UniswapV2Library.sortTokens(input, output);
IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
uint amountInput;
uint amountOutput;
{ // portée pour éviter les erreurs de pile trop profonde
(uint reserve0, uint reserve1,) = pair.getReserves();
(uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
En raison des frais de transfert, nous ne pouvons pas compter sur la fonction getAmountsOut pour nous dire combien nous obtenons de chaque transfert (comme nous le faisons avant d'appeler la fonction originale _swap). À la place, nous devons d'abord transférer, puis 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 finit par être annulé parce qu'il n'y a pas assez à la fin pour atteindre le minimum requis), cela finirait par coûter plus de gaz. Les jetons avec frais de transfert sont assez rares, donc bien que nous devions les prendre en compte, il n'est pas nécessaire que tous les échanges supposent qu'ils passent par au moins l'un d'entre eux.
}
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
pair.swap(amount0Out, amount1Out, to, new bytes(0));
}
}
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) {
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
payable
ensure(deadline)
{
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
uint amountIn = msg.value;
IWETH(WETH).deposit{value: amountIn}();
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
_swapSupportingFeeOnTransferTokens(path, to);
require(
IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
);
}
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
external
virtual
override
ensure(deadline)
{
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
);
_swapSupportingFeeOnTransferTokens(path, address(this));
uint amountOut = IERC20(WETH).balanceOf(address(this));
require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
IWETH(WETH).withdraw(amountOut);
TransferHelper.safeTransferETH(to, amountOut);
}
Ce sont les mêmes variantes utilisées pour les jetons normaux, mais elles appellent _swapSupportingFeeOnTransferTokens à la place.
// **** FONCTIONS DE LA BIBLIOTHÈQUE ****
function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
return UniswapV2Library.quote(amountA, reserveA, reserveB);
}
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountOut)
{
return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
}
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
public
pure
virtual
override
returns (uint amountIn)
{
return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
}
function getAmountsOut(uint amountIn, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsOut(factory, amountIn, path);
}
function getAmountsIn(uint amountOut, address[] memory path)
public
view
virtual
override
returns (uint[] memory amounts)
{
return UniswapV2Library.getAmountsIn(factory, amountOut, path);
}
}
Ces fonctions ne sont que des proxys 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, il n'est plus pertinent.
Les bibliothèques
La bibliothèque SafeMath (opens in a new tab) est bien documentée, il n'est donc pas nécessaire de la documenter ici.
Math
Cette bibliothèque contient des fonctions mathématiques qui ne sont normalement pas nécessaires dans le code Solidity, elles ne font donc pas partie du langage.
pragma solidity =0.5.16;
// une bibliothèque pour effectuer diverses opérations mathématiques
library Math {
function min(uint x, uint y) internal pure returns (uint z) {
z = x < y ? x : y;
}
// méthode babylonienne (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
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 particuliers).
while (x < z) {
z = x;
x = (y / x + x) / 2;
Obtenez une estimation plus précise, la moyenne de l'estimation précédente et du nombre dont nous essayons de trouver la racine carrée divisé par l'estimation précédente. Répétez jusqu'à ce que la nouvelle estimation ne soit pas inférieure à l'existante. Pour plus de détails, voir ici (opens in a new tab).
}
} else if (y != 0) {
z = 1;
Nous ne devrions jamais avoir besoin de la racine carrée de zéro. Les racines carrées de un, deux et trois valent environ un (nous utilisons des entiers, nous ignorons donc la fraction).
}
}
}
Fractions à virgule fixe (UQ112x112)
Cette bibliothèque gère les fractions, qui ne font normalement pas partie de l'arithmétique d'Ethereum. Elle le fait en encodant le nombre x sous la forme x*2^112. Cela nous permet d'utiliser les codes d'opération d'addition et de soustraction d'origine sans modification.
pragma solidity =0.5.16;
// une bibliothèque pour manipuler les nombres binaires à virgule fixe (https://wikipedia.org/wiki/Q_(number_format))
// plage : [0, 2**112 - 1]
// résolution : 1 / 2**112
library UQ112x112 {
uint224 constant Q112 = 2**112;
Q112 est l'encodage pour un.
// encoder un uint112 en UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // ne déborde jamais
}
Puisque y est uint112, sa valeur maximale est 2^112-1. Ce nombre peut toujours être encodé comme un UQ112x112.
// diviser un UQ112x112 par un uint112, renvoyant un UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
Si nous divisons deux valeurs UQ112x112, le résultat n'est plus multiplié par 2^112. À la place, nous prenons donc un entier pour le dénominateur. Nous aurions dû utiliser une astuce similaire pour faire une multiplication, mais nous n'avons pas besoin de multiplier des valeurs UQ112x112.
UniswapV2Library
Cette bibliothèque est utilisée uniquement par les contrats périphériques
pragma solidity >=0.5.0;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import "./SafeMath.sol";
library UniswapV2Library {
using SafeMath for uint;
// renvoie les adresses de jetons triées, utilisé pour gérer les valeurs de retour des paires triées dans cet ordre
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
}
Triez les deux jetons par adresse, afin que nous puissions obtenir l'adresse de l'échange de la paire pour eux. C'est nécessaire car sinon nous aurions deux possibilités, une pour les paramètres A,B et une autre pour les paramètres B,A, ce qui conduirait à deux échanges au lieu d'un.
// calcule l'adresse CREATE2 pour une paire sans faire d'appels externes
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // hachage du code d'initialisation
))));
}
Cette fonction calcule l'adresse de l'échange de la paire pour les deux jetons. Ce contrat est créé en utilisant le code d'opération CREATE2 (opens in a new tab), nous pouvons donc calculer l'adresse en utilisant le même algorithme si nous connaissons les paramètres qu'il utilise. C'est beaucoup moins cher que de demander à la factory, et
// récupère et trie les réserves pour une paire
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
Cette fonction renvoie les réserves des deux jetons que possède l'échange de la paire. Notez qu'elle peut recevoir les jetons dans n'importe quel ordre et les trie pour un usage interne.
// étant donné un certain montant d'un actif et les réserves de la paire, renvoie un montant équivalent de l'autre actif
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
Cette fonction vous donne la quantité de jeton B que vous obtiendrez en échange du jeton A s'il n'y a pas de frais impliqués. Ce calcul prend en compte le fait que le transfert modifie le taux de change.
// étant donné un montant d'entrée d'un actif et les réserves de la paire, renvoie le montant de sortie maximum de l'autre actif
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
La fonction quote ci-dessus fonctionne très bien s'il n'y a pas de frais pour utiliser l'échange de la paire. Cependant, s'il y a des frais d'échange de 0,3 %, le montant que vous obtenez réellement est inférieur. Cette fonction calcule le montant après les frais d'échange.
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
Solidity ne gère pas les fractions nativement, nous ne pouvons donc pas simplement multiplier le montant par 0,997. À la place, nous multiplions le numérateur par 997 et le dénominateur par 1000, obtenant ainsi le même effet.
// étant donné un montant de sortie d'un actif et les réserves de la paire, renvoie un montant d'entrée requis de l'autre actif
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}
Cette fonction fait à peu près la même chose, mais elle obtient le montant de sortie et fournit l'entrée.
// effectue des calculs getAmountOut enchaînés sur n'importe quel nombre de paires
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
// effectue des calculs getAmountIn enchaînés sur n'importe quel nombre de paires
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[amounts.length - 1] = amountOut;
for (uint i = path.length - 1; i > 0; i--) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
}
}
}
Ces deux fonctions gèrent l'identification des valeurs lorsqu'il est nécessaire de passer par plusieurs échanges de paires.
Transfer Helper
Cette bibliothèque (opens in a new tab) ajoute des vérifications de succès autour des transferts ERC-20 et Ethereum pour traiter une annulation et un retour de valeur false de la même manière.
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.6.0;
// méthodes d'aide pour interagir avec les jetons ERC-20 et envoyer de l'ETH qui ne renvoient pas systématiquement true/false
library TransferHelper {
function safeApprove(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('approve(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
Nous pouvons appeler un contrat différent de deux manières :
- Utiliser une définition d'interface pour créer un appel de fonction
- Utiliser l'interface binaire de l'application (ABI) (opens in a new tab) « manuellement » pour créer l'appel. C'est ce que l'auteur du code a décidé de faire.
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeApprove: approve failed'
);
}
Par souci de rétrocompatibilité avec les jetons créés avant la norme ERC-20, un appel ERC-20 peut échouer soit en étant annulé (auquel cas success est false), soit en réussissant et en renvoyant une valeur false (auquel cas il y a des données de sortie, et si vous les décodez comme un booléen, vous obtenez false).
function safeTransfer(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeTransfer: transfer failed'
);
}
Cette fonction implémente la fonctionnalité de transfert d'ERC-20 (opens in a new tab), qui permet à un compte de dépenser l'allocation fournie par un autre compte.
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}
Cette fonction implémente la fonctionnalité transferFrom d'ERC-20 (opens in a new tab), qui permet à un compte de dépenser l'allocation fournie par un autre compte.
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
}
}
Cette fonction transfère de l'ether vers un compte. Tout appel à un contrat différent peut tenter d'envoyer de l'ether. Comme nous n'avons pas besoin d'appeler réellement une fonction, nous n'envoyons aucune donnée avec l'appel.
Conclusion
Il s'agit d'un long article d'environ 50 pages. Si vous êtes arrivé jusqu'ici, félicitations ! J'espère que vous avez maintenant compris les enjeux liés à la création d'une application concrète (par opposition aux courts programmes d'exemple) et que vous êtes mieux préparé pour écrire des contrats pour vos propres cas d'utilisation.
Maintenant, allez créer quelque chose d'utile et impressionnez-nous.
