Uniswap v2 explained for the beginner(code base)
In this article, we will not simply introduce what Uniswap is, but instead, we will thoroughly analyze and present the inner workings of Uniswap V2. As such, basic knowledge of Solidity may be required!
If you find any mistakes or inaccuracies, your comments would be greatly appreciated!
We will mainly analyze the following components:
- UniswapV2Pair
- UniswapV2Factory
- UniswapV2Router02
- Liquidity
Liquidity
Liquidity is an incredibly important concept in Uniswap V2, so let’s briefly summarize it.
The liquidity calculation is at the core of the Automated Market Maker (AMM) algorithm. This algorithm is based on the following formula:
x * y = k
Liquidity providers deposit two tokens into a pool and receive liquidity tokens in return. The AMM algorithm calculates liquidity using the formula x * y = k. Providers add liquidity using the mint function and withdraw liquidity using the burn function. Since the liquidity in Uniswap V2 affects price slippage and transaction fees, maintaining an appropriate balance is crucial, making liquidity a highly important concept.
UniswapV2 Pair
initialize
This function is used to set the two tokens when creating a pair.
Instead of using a constructor, the initialize function is used for the factory pattern.
UniswapV2Factory is a single contract from which multiple pairs can be created. To implement the factory pattern, the initialize function is used. By using the initialize function, unique initial settings can be performed for each pair after deploying the pair contract. This allows the Factory contract to generate and manage the pair contracts efficiently.
Arguments:
_token0address: The address of the first token in the pair
_token1address: The address of the second token in the pair
function initialize(address _token0, address _token1) external {
// Ensure that UniswapV2Factory is executing the pair
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
// Set each token
token0 = _token0;
token1 = _token1;
}
mint
The mint function is used when a new liquidity provider adds tokens to the pair. It issues liquidity tokens and allocates them to the liquidity provider.
Liquidity calculation is a crucial concept, so let’s briefly explain it.
Liquidity calculation is based on the following equation:
x * y = k
By ensuring that the product (k) of the reserve balances (x and y) of the pair remains constant, the rate of each token is determined by trading. This calculation is at the core of AMMs and is a crucial concept.
Arguments:
to address: The address receiving the liquidity tokens
function mint(address to) external lock returns (uint liquidity) {
// Get current reserves
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
// gas savings
// Check token0 balance
uint balance0 = IERC20(token0).balanceOf(address(this));
// Check token1 balance
uint balance1 = IERC20(token1).balanceOf(address(this));
// Calculate total token balance using _reserve0 - reserve0
uint amount0 = balance0.sub(_reserve0);
// Calculate total token balance using _reserve1 - reserve1
uint amount1 = balance1.sub(_reserve1);
// Calculate and accumulate fees, and get whether fees are applied as a bool
bool feeOn = _mintFee(_reserve0, _reserve1);
// Assign to local variable for gas savings
uint _totalSupply = totalSupply;
// gas savings, must be defined here since totalSupply can update in _mintFee
// When _totalSupply is zero
if (_totalSupply == 0) {
// Calculate initial liquidity based on the square root of the product of added token amounts
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
// Permanently lock the first MINIMUM_LIQUIDITY tokens at address 0
_mint(address(0), MINIMUM_LIQUIDITY);
// permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
// Calculate new liquidity tokens based on the ratio of added tokens to existing reserves
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
// Ensure that the issued liquidity is greater than 0
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
// Issue liquidity tokens to the specified address
_mint(to, liquidity);
// Update reserves with the new token balances
_update(balance0, balance1, _reserve0, _reserve1);
// Update kLast if protocol fees are enabled
if (feeOn) kLast = uint(reserve0).mul(reserve1);
// reserve0 and reserve1 are up-to-date
// Emit Mint event with sender and added token amounts
emit Mint(msg.sender, amount0, amount1);
}
burn
The burn function is used when liquidity providers withdraw liquidity. Liquidity providers destroy liquidity tokens and withdraw tokens from the reserve. The function updates token balances and reserves and calculates fees as needed.
In short, it’s the process of withdrawing each token you’ve deposited by returning LP tokens.
Arguments:
to address: The address to receive the tokens
function burn(address to) external lock returns (uint amount0, uint amount1) {
// Obtain current reserves
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
// gas savings
address _token0 = token0;
// gas savings
address _token1 = token1;
// gas savings
// Obtain token0 balance
uint balance0 = IERC20(_token0).balanceOf(address(this));
// Obtain token1 balance
uint balance1 = IERC20(_token1).balanceOf(address(this));
// Obtain the amount of liquidity tokens the sender has
uint liquidity = balanceOf[address(this)];
// Calculate and accumulate fees, and obtain whether the fee is applied as a bool
bool feeOn = _mintFee(_reserve0, _reserve1);
// Assign to a local variable once for gas saving
uint _totalSupply = totalSupply;
// gas savings, must be defined here since totalSupply can update in _mintFee
// Calculate the amount of tokens to withdraw
amount0 = liquidity.mul(balance0) / _totalSupply;
// using balances ensures pro-rata distribution
// Calculate the amount of tokens to withdraw
amount1 = liquidity.mul(balance1) / _totalSupply;
// using balances ensures pro-rata distribution
// Ensure that each amount to withdraw is greater than zero
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
// Burn the sender's liquidity tokens
_burn(address(this), liquidity);
// Send the withdrawn tokens to 'to'
_safeTransfer(_token0, to, amount0);
// Calculate the amount of tokens to withdraw
_safeTransfer(_token1, to, amount1);
// Obtain the token holdings after withdrawal
balance0 = IERC20(_token0).balanceOf(address(this));
// Obtain the token holdings after withdrawal
balance1 = IERC20(_token1).balanceOf(address(this));
// Update reserves with the trading information from above
_update(balance0, balance1, _reserve0, _reserve1);
// Update kLast if protocol fees are enabled
if (feeOn) kLast = uint(reserve0).mul(reserve1);
// reserve0 and reserve1 are up-to-date
// Emit Burn event with sender and burned token amounts
emit Burn(msg.sender, amount0, amount1, to);
}
swap
The swap function is used when trading between tokens. This function provides prices between two different tokens by exchanging tokens within the reserve, updating the reserve, and calculating fees as needed.
Arguments:
amount0Out unit : Amount of token0 to be exchanged
amount1Out unit : Amount of token1 to be exchanged
to address : Address to receive the exchanged tokens
data bytes : Additional data (as needed)
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
// Ensure output amount is greater than zero
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
// Obtain each reserve
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
// gas savings
// Ensure each output amount is smaller than the reserve
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{// scope for _token{0,1}, avoids stack too deep errors
// Store token addresses in variables
address _token0 = token0;
address _token1 = token1;
// Ensure destination address is different from token addresses
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
// Send token0 if amount0Out is greater than zero
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
// Send token1 if amount1Out is greater than zero
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
// Execute custom logic (optional)
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
// Obtain balance
balance0 = IERC20(_token0).balanceOf(address(this));
// Obtain balance
balance1 = IERC20(_token1).balanceOf(address(this));
}
// Calculate input amount
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
// Ensure each input amount is greater than zero
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{// scope for reserve{0,1}Adjusted, avoids stack too deep errors
// Calculate adjusted token balance after swap
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
// Ensure K value does not decrease compared to pre-swap token balances
// Prevents price slippage and malicious trading
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000 ** 2), 'UniswapV2: K');
}
// Update reserves with the trading information from above
_update(balance0, balance1, _reserve0, _reserve1);
// Notify Swap event
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
Uniswap V2Factory
Uniswap V2Factory is mainly responsible for creating and managing token pairs.
Let’s take a look at the contents of the createPair
function. Each line explains what the process is doing, so if you're interested, feel free to take a closer look.
createPair
function createPair(address tokenA, address tokenB) external returns (address pair) {
// Ensure tokenA and tokenB are not the same
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
// Compare tokenA and tokenB, assign the larger one to token0 and the smaller one to token1
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// Ensure token0's address is not zero
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
// Check if the pair of token0 and token1 does not exist
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
// Obtain the bytecode of UniswapV2Pair
bytes memory bytecode = type(UniswapV2Pair).creationCode;
// Create salt by concatenating the addresses of token0 and token1 and hashing them
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
// Calculate the contract address before deployment using create2
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
// Initialize the UniswapV2Pair
IUniswapV2Pair(pair).initialize(token0, token1);
// Store the address of the pair of token0 and token1 in the pair map
getPair[token0][token1] = pair;
// Do the same for the reverse token1 & token0
getPair[token1][token0] = pair; // populate mapping in the reverse direction
// Add the pair address to the allPairs array
allPairs.push(pair);
// Notify of pair creation via emit
emit PairCreated(token0, token1, pair, allPairs.length);
}
The content is not too complicated, and it mainly searches for an existing pair, and if it doesn’t exist, it creates a new Pair.
The usage is also simple, and you only need to input the addresses of the tokens you want to pair! It looks something like this when written very simply:
IUniswapV2Factory factory;
address pair;
function create(address _tokenA, address _tokenB) external {
pair = factory.createPair(_tokenA, _tokenB);
}
So, UniswapV2Pair only creates the pair, while UniswapV2Factory manages the pairs. Pair management is just simple management, like which pairs were created or which pairs are held.
If you want to perform operations such as swap
or burn
on the tokens in the Pair, you'll use UniswapV2Pair.
setFeeTo & setFeeToSetter
setFeeTo
is the address of the recipient of the fees generated across the entire protocol. In Uniswap V2, a fixed fee (default 0.3%) is applied to each trade, and this fee is added to the liquidity pool. As a result, liquidity providers can receive these fees as revenue when withdrawing from the pool.
feeToSetter
is the address with the authority to set and change the feeTo
address (fee recipient). The feeToSetter
is usually set by the protocol's governance or administrators, allowing parties involved in the operation and development of the protocol to properly set and change the fee recipient.
So, it’s about deciding who receives the fees and who manages them.
UniswapV2Router02
Uniswap V2 Router02 provides the interface for Uniswap V2, making it easier to exchange tokens, add, and remove liquidity. Let’s go through it step by step.
Mainly,
addLiquidity
This function is used to add liquidity to any ERC20 token pair pool. Let’s take a look at how it’s implemented!
First, let’s start with the arguments.
Argument
tokenA : Address of the ERC-20 token
tokenB : Address of the second ERC-20 token
amountADesired : Desired amount of token A to provide
amountBDesired : Desired amount of token B to provide
amountAMin : Minimum amount of token A to provide, considering slippage
amountBMin : Minimum amount of token B to provide, considering slippagetoAddress to receive LP tokens
deadline : Deadline for the transaction (UNIX timestamp)
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
// Pass all arguments and calculate the optimal amount of tokens
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
// Predict and get the pair token address
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// Send tokenA to the pair contract
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
// Send tokenB to the pair contract
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
// Issue liquidity tokens to 'to' here
liquidity = IUniswapV2Pair(pair).mint(to);
}
What it does:
- Calculate the optimal amount of tokens
- Generate pair token address
- Send tokens to the pair contract
- Issue liquidity tokens
This function may seem to do little at first glance, but the _addLiquidity
the function does a lot.solidityCopy code
/// **** ADD LIQUIDITY ****
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// Check if the liquidity token pair being added exists
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
// Create a new pair if it doesn't exist
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
// Get the reserves of the target pair
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
// Check if both reserves are 0
if (reserveA == 0 && reserveB == 0) {
// If both reserves are zero, insert the desired token amounts into the pair
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
// If the pair is not both zero, calculate the optimal token amount for amountB from the pair's reserves
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
// If the optimal token amount calculated above is less than the desired token amount
if (amountBOptimal <= amountBDesired) {
// Error if the optimal token amount is less than the minimum token amount
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
// Here, the token amount for amountB becomes the optimal token amount
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
// If the optimal token amount is less than the desired token amount
// Calculate the optimal token amount for amountA from the pair's reserves
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
// Error if amountAOptimal is greater than amountADesired
// Here, assert is used, so gas is consumed if it fails
assert(amountAOptimal <= amountADesired);
// Error if the optimal token amount is less than the minimum token amount
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
// Here, the token amount for amountA becomes the optimal token amount
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}
addLiquidityETH
The reason why we need a separate function for ETH is that ETH is not an ERC20 token, so the process requires converting ETH to Wrapped Ether (WETH) to make it compatible with ERC20.
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
// Pass all arguments and calculate the optimal token amount
(amountToken, amountETH) = _addLiquidity(
token,
WETH,
amountTokenDesired,
msg.value,
amountTokenMin,
amountETHMin
);
// Predict and get the address of the pair token
address pair = UniswapV2Library.pairFor(factory, token, WETH);
// Send the ERC20 token to the pair contract
TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
// Convert ETH to WETH
IWETH(WETH).deposit{value: amountETH}();
// Send WETH to the pair contract
assert(IWETH(WETH).transfer(pair, amountETH));
// Issue liquidity tokens to 'to'
liquidity = IUniswapV2Pair(pair).mint(to);
// Refund any dust ETH, if any
// Return the remainder if there is a difference between the actual sent amount and the used amount
if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}
As you can see from the above, this is a crucial function for adding liquidity to a pool in a decentralized exchange (DEX)!
removeLiquidity
This function is used to withdraw liquidity from a liquidity pool. It is used by liquidity providers (LPs) to exit their liquid positions and exchange their held liquidity tokens for the two tokens corresponding to the pair.
The arguments are as follows:
- tokenA: The address of the first token to be withdrawn from the liquidity pool
- tokenB: The address of the second token to be withdrawn from the liquidity pool
- liquidity: The amount of liquidity tokens to be withdrawn
- amountAMin: The minimum amount of token A to be withdrawn. This is used for slippage protection.
- amountBMin: The minimum amount of token B to be withdrawn. This is also used for slippage protection.
- to: The address where the withdrawn tokens will be sent
- deadline: The expiration time of the transaction (UNIX timestamp)
Let’s take a look at the actual function contents!javascriptCopy code
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) {
// Get the address of the target pair contract
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// Send the liquidity tokens to the pair
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
// Burn the liquidity tokens of the pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
// Sort tokenA and tokenB and arrange them in descending order
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
// Sort the amounts
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
// Verify that the withdrawn amount of tokenA is greater than or equal to the minimum amount
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
// Verify that the withdrawn amount of tokenB is greater than or equal to the minimum amount
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
This function burns the specified liquidity tokens and returns the corresponding two tokens to the provider. As slippage protection, if the actual withdrawal amounts are lower than amountAMin and amountBMin, the transaction will fail. This prevents losses due to unexpected price fluctuations.
Let’s also take a look at addLiquidityETH, which also supports ETH!
addLiquidityETH
By using this function, users can deposit ERC-20 tokens and Ether into a liquidity pool and receive liquidity provider (LP) tokens corresponding to the pair. LP tokens are used when users want to withdraw liquidity from the pool.
- token: The address of the ERC20 token to be withdrawn
- liquidity: The amount of liquidity tokens to be withdrawn
- amountTokenMin: The minimum number of tokens to be withdrawn, taking slippage into account
- amountETHMin: The minimum amount of Ether to be withdrawn, taking slippage into account
- to: The address receiving the tokens and Ether
- deadline: The expiration time of the transaction (UNIX timestamp)
The removeLiquidityETH function sends LP tokens to the Uniswap V2 pair contract and returns tokens and Ether to the user. This function internally calls the _removeLiquidity function to handle the actual liquidity withdrawal logic. Also, this function uses WETH (Wrapped Ether) to treat Ether as an ERC-20 token, calling the withdraw function from the IWETH interface to convert WETH to Ether.
Note that the removeLiquidityETH function can only withdraw liquidity from existing liquidity pools. To create a new liquidity pool or add liquidity, you need to use other functions like addLiquidityETH.
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
// Since removeLiquidity cannot be used with ETH, WETH is used as a substitute to follow the original process flow
(amountToken, amountETH) = removeLiquidity(
token,
WETH,
liquidity,
amountTokenMin,
amountETHMin,
address(this),
deadline
);
// Send the withdrawn tokens to the target address
TransferHelper.safeTransfer(token, to, amountToken);
// Convert WETH to ETH
IWETH(WETH).withdraw(amountETH);
// Return ETH
TransferHelper.safeTransferETH(to, amountETH);
}
In practice, the process is not much different from removeLiquidity, only handling ETH more conveniently.
_swap
Next, let’s discuss the swap-related functions, starting with the common _swap function.
This function is a common function used by all swap-related functions. It is an internal function of the Uniswap V2 Router02, and contains the logic for executing trades.
// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
// Loop through the array of addresses to swap
for (uint i; i < path.length - 1; i++) {
// Assign the first address and the next address in the array as input and output variables
(address input, address output) = (path[i], path[i + 1]);
// Sort input and output tokens and get token0 (the smaller address)
(address token0,) = UniswapV2Library.sortTokens(input, output);
// Get the output amount for the current pair
uint amountOut = amounts[i + 1];
// If input == token0, set amount0Out to zero and amount1Out to amountOut
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
// Set the recipient address for the trade. If not the last pair, set the address of the next pair, and if it is the last pair, use the _to address
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
// Call the swap function for the current pair and execute the trade
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
amount0Out, amount1Out, to, new bytes(0)
);
}
}
swapExactTokensForTokens
The swapExactTokensForTokens function is used to buy a minimum amount of output tokens using an exact amount of input tokens. By using this function, users can easily execute trades between two different tokens.
- amountIn: The exact amount of input tokens
- amountOutMin: The minimum amount of output tokens to receive, used to handle slippage
- path: An array of token addresses used for the trade. For each consecutive pair, the tokens in the path must be both tokens of the pair
- to: The address receiving the output tokens
- deadline: The expiration time of the transaction (UNIX timestamp)
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
// Calculate the predicted output amounts for the tokens to be swapped
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
// Ensure the predicted amount is greater than or equal to the minimum output amount
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
// Transfer the tokens to be swapped to the pair contract
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
// Execute the swap process
_swap(amounts, path, to);
}
swapTokensForExactTokens
This function allows you to trade one token for another by specifying the exact amount of output tokens you want to receive. It calculates the amount of input tokens needed to obtain the specified output tokens by navigating through a series of token pairs, and then executes the actual trade.
Arguments:
amountOut — The exact amount of output tokens you want to receive after the trade amountInMax — The maximum allowed input tokens (considering slippage) path — An array of addresses representing the trading path (e.g., [Token A, Token B, Token C]) to — The address that will receive the output tokens deadline — The transaction deadline (Unix timestamp)
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
// Calculate the input token amount required to obtain a specific output token amount
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
// Ensure the input token amount is not greater than the maximum input amount
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
// Transfer the tokens to be swapped to the pair contract
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
// Execute the swap
_swap(amounts, path, to);
}
swapExactTokensForETH
The swapExactTokensForETH function is designed to swap the exact amount of input tokens for ETH within the Uniswap V2 Router02 contract. It can navigate through multiple token pairs for trading.
Arguments:
- amountIn : The exact amount of input tokens
- amountOutMin : The minimum allowed amount of ETH to receive (considering slippage)
- path : An array of addresses representing the trading path (e.g., [Token A, Token B, WETH])
- to : The address that will receive the output tokens (ETH)
- deadline : The transaction deadline (Unix timestamp)
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) external virtual override ensure(deadline) returns (uint[] memory amounts){
// Ensure the last element in the path is WETH
require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
// Calculate the required input token amount for a specific output token amount
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
// Ensure the input token amount is not greater than the maximum input amount
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
// Transfer the tokens to be swapped to the pair contract
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
// Perform the swap
_swap(amounts, path, address(this));
// Convert WETH back to ETH
IWETH(WETH).withdraw(amounts[amounts.length - 1]);
// Transfer the ETH to the recipient
TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}
swapExactETHForTokens
The swapExactETHForTokens function allows you to use a specified amount of ETH to perform a swap between tokens specified in the path and send the final obtainable tokens to the specified address.
Using this function, users can consecutively swap between tokens specified in the path using ETH, ultimately obtaining the desired token. This makes it easy to exchange ETH for other tokens. Additionally, set the amountOutMin to control slippage, and use the deadline to ensure the transaction completes before it expires.
Arguments:
- amountOutMin : The minimum token amount to be obtained in the swap. This is used to control slippage.
- path : The token exchange path. The first element must be ETH, and the last element must be the target token.
- to : The address where the finally obtained tokens will be sent.
- deadline : The transaction validity deadline (UNIX timestamp). Transactions will fail if the deadline is exceeded.
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external virtual override payable ensure(deadline) returns (uint[] memory amounts){
// Ensure the first element in the path is WETH
require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
// Calculate the predicted output token amount for the swap
amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
// Ensure the last amount in amounts is greater than or equal to the minimum output amount
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
// Convert ETH to WETH
IWETH(WETH).deposit{value: amounts[0]}();
// Ensure WETH is transferred to the pair contract and transfer it
assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
// Perform the swap
_swap(amounts, path, to);
}
Finally, I’m currently enrolled in a course by JohnnyTime called “smartcontractshacking" https://smartcontractshacking.com/. This course offers a highly effective approach to learning web3 security. In fact, the idea for my article emerged while taking this course! Give it a look and give it a try!