요즘 같은 밈코인 장에 아주 적절한 예제를 만나서 너무 반갑다. 실제 스나이핑 봇에 비하면 허술하겠지만 그래도 만들다보면 얻는 게 많을 것 같다. 유니스왑 코드를 참고해서 아래 빈칸들을 채워보자.
pragma solidity 0.8.13;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "../interfaces/IUniswapV2.sol";
/**
* @title Sniper
* @author JohnnyTime (https://smartcontractshacking.com)
*/
contract Sniper is Ownable {
// We are calculating `amountOut = amountOut * slippage / 1000.
// So, if we set slippage to 1000, it means no slippage at all, because 1000 / 1000 = 1
// Initally we try to purchase without slippage
uint private constant INITIAL_SLIPPAGE = 1000;
// Every failed attemp we will increase the slippage by 0.3% (3 / 1000)
uint private constant SLLIPAGE_INCREMENTS = 3;
IUniswapV2Factory private immutable factory;
constructor(address _factory) {
factory = IUniswapV2Factory(_factory);
}
/**
* The main external snipe function that is being called by the contract owner.
* Checks the the current reserves, determines the expected amountOut.
* If amountOut >= `_absoluteMinAmountOut`, it will try to swap the tokens `_maxRetries` times
* using the internal `_swapWithRetries` function.
* @param _tokenIn the token address you want to sell
* @param _tokenOut the token address you want to buy
* @param _amountIn the amount of tokens you are sending in
* @param _absoluteMinAmountOut the minimum amount of tokens you want out of the trade
* @param _maxRetries In case the swap fails, it will try again _maxRetries times (with higher slippage tolerance every time)
*/
function snipe(
address _tokenIn, address _tokenOut, uint256 _amountIn,
uint256 _absoluteMinAmountOut, uint8 _maxRetries)
external onlyOwner {
// TODO: Implement this function
// TODO: Use the Factory to get the pair contract address, revert if pair doesn't exist
// Note: might return error - if the pair is not created yet
// TODO: Sort the tokens using the internal `_sortTokens` function
uint256 amountOut;
// NOTE: We're using block to avoid "stack too deep" error
{
// TODO: Get pair reserves, and match them with _tokenIn and _tokenOut
// TODO: Get the expected amount out and revert if it's lower than the `_absoluteMinAmountOut`
// NOTE: Use the internal _getAmountOut function
}
// TODO: Transfer the token to the pair contract
// TODO: Set amount0Out and amount1Out, based on token0 & token1
// TODO: Call internal _swapWithRetreis function with the relevant parameters
// TODO: Transfer the sniped tokens to the owner
}
/**
* Internal function that will try to swap the tokens using the pair contract
* In case the swap failed, it will call itself again with higher slippage
* and try again until the swap succeded or `_maxRetries`
*/
function _swapWithRetries(
address _pair, uint256 _amount0Out, uint256 _amount1Out, uint8 _maxRetries, uint8 _retryNo
) internal {
// TODO: Implement this function
// Our slippage tolerance. Every retry we will be willinig to pay 0.3% more for the tokens
// The slippage will be calculated by `amountOut * slippage / 1000`, so
// 0.3% = 997, 0.6% = 994, and so on..
uint256 slippageTolerance;
// TODO: Revert if we reached max retries
// TODO: Set the slippage tolerance based on the _retryNo
// TODO: Start from INITIAL_SLIPPAGE, then every retry we reduce SLLIPAGE_INCREMENTS
// TODO: Apply the slippage to the amounts
// TODO: Call the low-level pair swap() function with all the parameters
// TODO: In case it failed, call _swapWithRetreis again (don't forget to increment _retryNo)
}
/**
* Internal function to sort the tokens by their addresses
* Exact same logic like in the Unsiwap Factory `createPair()` function.
*/
function _sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
// TODO: Implement tokens sorting functionality as in Uniswap V2 Factory `createPair` function
}
/**
* Internal function to get the expected amount of tokens which we will receive based on given `amountIn` and pair reserves.
* Exact same logic like in the Unsiwap Library `_getAmountOut()` function.
*/
function _getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
// TODO: Implement functionality as in Uniswap V2 Library `getAmountOut` function
}
}
function snipe(
address _tokenIn, address _tokenOut, uint256 _amountIn,
uint256 _absoluteMinAmountOut, uint8 _maxRetries)
external onlyOwner {
// TODO: Implement this function
// TODO: Use the Factory to get the pair contract address, revert if pair doesn't exist
// Note: might return error - if the pair is not created yet
address pair = factory.getPair(_tokenIn, _tokenOut);
if (pair == address(0)) {
revert();
}
// TODO: Sort the tokens using the internal `_sortTokens` function
(address token0, address token1) = _sortTokens(_tokenIn, _tokenOut);
uint256 amountOut;
// NOTE: We're using block to avoid "stack too deep" error
{
// TODO: Get pair reserves, and match them with _tokenIn and _tokenOut
(uint112 _reserve0, uint112 _reserve1,) = pair.getReserves();
// TODO: Get the expected amount out and revert if it's lower than the `_absoluteMinAmountOut`
// NOTE: Use the internal _getAmountOut function
amountOut = _getAmountOut(_amountIn, _reserve0, _reserve1);
if (amountOut < _absoluteMinAmountOut) {
revert();
}
}
// TODO: Transfer the token to the pair contract
IERC20(_tokenIn).transferFrom(msg.sender, pair, _amountIn);
// TODO: Set amount0Out and amount1Out, based on token0 & token1
(uint256 amount0Out, uint256 amount1Out) = ??
// TODO: Call internal _swapWithRetreis function with the relevant parameters
_swapWithRetries(pair, amount0Out, amount1Out, _maxRetries, 0);
// TODO: Transfer the sniped tokens to the owner
IERC20(_tokenOut).transfer(msg.sender, amountOut);
}
중간에 amount0Out
, amount1Out
을 어떻게 해야될 지 몰라서 비워놨다.
function snipe(
address _tokenIn,
address _tokenOut,
uint256 _amountIn,
uint256 _absoluteMinAmountOut,
uint8 _maxRetries
) external onlyOwner {
// TODO: Implement this function
// TODO: Use the Factory to get the pair contract address, revert if pair doesn't exist
// Note: might return error - if the pair is not created yet
address pairAddress = factory.getPair(_tokenIn, _tokenOut);
require(pairAddress != address(0), "Pair doesnt exist");
pair = IUniswapV2Pair(pairAddress);
// TODO: Sort the tokens using the internal `_sortTokens` function
(address token0, address token1) = _sortTokens(_tokenIn, _tokenOut);
uint256 amountOut;
// NOTE: We're using block to avoid "stack too deep" error
{
// TODO: Get pair reserves, and match them with _tokenIn and _tokenOut
(uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
(uint256 reserveIn, uint256 reserveOut) = _tokenIn == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
// TODO: Get the expected amount out and revert if it's lower than the `_absoluteMinAmountOut`
// NOTE: Use the internal _getAmountOut function
amountOut = _getAmountOut(_amountIn, reserveIn, reserveOut);
require(amountOut >= _absoluteMinAmountOut);
}
// TODO: Transfer the token to the pair contract
IERC20(_tokenIn).transfer(address(pair), _amountIn);
// TODO: Set amount0Out and amount1Out, based on token0 & token1
(uint256 amount0Out, uint256 amount1Out) = _tokenIn == token0
? (uint256(0), amountOut)
: (amountOut, uint256(0));
// TODO: Call internal _swapWithRetries function with the relevant parameters
_swapWithRetries(pairAddress, amount0Out, amount1Out, _maxRetries, 0);
// TODO: Transfer the sniped tokens to the owner
IERC20(_tokenOut).transfer(owner(), IERC20(_tokenOut).balanceOf(address(this)));
}
수정해야 할 부분은 block
부분이다.
// NOTE: We're using block to avoid "stack too deep" error
{
// TODO: Get pair reserves, and match them with _tokenIn and _tokenOut
(uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
(uint256 reserveIn, uint256 reserveOut) = _tokenIn == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
// TODO: Get the expected amount out and revert if it's lower than the `_absoluteMinAmountOut`
// NOTE: Use the internal _getAmountOut function
amountOut = _getAmountOut(_amountIn, reserveIn, reserveOut);
require(amountOut >= _absoluteMinAmountOut);
}
getReserves()
를 통해 reserve를 가져왔다고 해서 그걸로 끝나면 안 된다. 어떤 reserve가 페어에서 첫 번째로 사용될 지 정렬해줘야 한다. 이후에도 받는 토큰의 순서(amountOut
을 앞에다 할 지 뒤에다 할 지)를 설정해줘야 한다
IERC20(_tokenIn).transfer(address(pair), _amountIn);
나는 transferFrom
을 이용해서 pair에 토큰을 보내게 했었는데 모범답안에서는 페어에 이미 토큰이 있다고 보고 그냥 transfer
를 이용해 보내는 것 같다.
내가 쓴 답
// TODO: Transfer the sniped tokens to the owner
IERC20(_tokenOut).transfer(msg.sender, amountOut);
모범 답안
IERC20(_tokenOut).transfer(owner(), IERC20(_tokenOut).balanceOf(address(this)));
마지막 부분에서 나는 amountOut
을 그냥 msg.sender
에게 보내게 해놨는데, 이렇게 될 경우 스왑 이후의 받은 토큰의 양이 amountOut
보다 적을 수 있다. 따라서 모범 답안처럼 스왑 이후에 컨트랙트가 가지고 있는 토큰의 양을 모두 보내는 방법이 훨씬 낫다.
function _swapWithRetries(
address _pair, uint256 _amount0Out, uint256 _amount1Out, uint8 _maxRetries, uint8 _retryNo
) internal {
// TODO: Implement this function
// Our slippage tolerance. Every retry we will be willinig to pay 0.3% more for the tokens
// The slippage will be calculated by `amountOut * slippage / 1000`, so
// 0.3% = 997, 0.6% = 994, and so on..
uint256 slippageTolerance;
// TODO: Revert if we reached max retries
if (_retryNo > _maxRetries) {
revert();
}
// TODO: Set the slippage tolerance based on the _retryNo
// TODO: Start from INITIAL_SLIPPAGE, then every retry we reduce SLLIPAGE_INCREMENTS
uint256 adjustment = SLLIPAGE_INCREMENTS * _retryNo;
slippageTolerance = (INITIAL_SLIPPAGE.sub(adjustment)) / 1000;
// TODO: Apply the slippage to the amounts
_amount0Out = _amount0Out * slippageTolerance;
_amount1Out = _amount1Out * slippageTolerance;
// TODO: Call the low-level pair swap() function with all the parameters
// TODO: In case it failed, call _swapWithRetreis again (don't forget to increment _retryNo)
(bool success, ) = _pair.swap(_amount0Out, _amount1Out, msg.sender, 0);
if (success == false) {
_retryNo++;
_swapWithRetries(_pair, _amount0Out, _amount1Out, _maxRetries, _retryNo);
}
}
function _swapWithRetries(
address _pair,
uint256 _amount0Out,
uint256 _amount1Out,
uint8 _maxRetries,
uint8 _retryNo
) internal {
// TODO: Implement this function
// Our slippage tolerance. Every retry we will be willinig to pay 0.3% more for the tokens
// The slippage will be calculated by `amountOut * slippage / 1000`, so
// 0.3% = 997, 0.6% = 994, and so on..
uint256 slippageTolerance;
// TODO: Revert if we reached max retries
require(_retryNo < _maxRetries, "Reached Max Retries");
// TODO: Set the slippage tolerance based on the _retryNo
// TODO: Start from INITIAL_SLIPPAGE, then every retry we reduce SLLIPAGE_INCREMENTS
slippageTolerance = INITIAL_SLIPPAGE - (SLIPPAGE_INCREMENTS * _retryNo);
// TODO: Apply the slippage to the amounts
_amount0Out = (_amount0Out * slippageTolerance) / 1000;
_amount1Out = (_amount1Out * slippageTolerance) / 1000;
// TODO: Call the low-level pair swap() function with all the parameters
// TODO: In case it failed, call _swapWithRetreis again (don't forget to increment _retryNo)
try pair.swap(_amount0Out, _amount1Out, address(this), new bytes(0)) {} catch {
_swapWithRetries(_pair, _amount0Out, _amount1Out, _maxRetries, _retryNo++);
}
}
마지막 부분에서 다시 swap
을 할 때 모범 답안에서는 try - catch
를 사용했다. 우선 이렇게도 할 수 있다는 것만 알아두자.
function _sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
// TODO: Implement tokens sorting functionality as in Uniswap V2 Factory `createPair` function
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
}
/**
* Internal function to get the expected amount of tokens which we will receive based on given `amountIn` and pair reserves.
* Exact same logic like in the Unsiwap Library `_getAmountOut()` function.
*/
function _getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
// TODO: Implement functionality as in Uniswap V2 Library `getAmountOut` function
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;
}
function _sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
// TODO: Implement tokens sorting functionality as in Uniswap V2 Factory `createPair` function
require(tokenA != tokenB, "UniswapV2: IDENTICAL_ADDRESSES");
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), "UniswapV2: ZERO_ADDRESS");
}
/**
* Internal function to get the expected amount of tokens which we will receive based on given `amountIn` and pair reserves.
* Exact same logic like in the Unsiwap Library `_getAmountOut()` function.
*/
function _getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
// TODO: Implement functionality as in Uniswap V2 Library `getAmountOut` function
require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
uint amountInWithFee = amountIn * 997;
uint numerator = amountInWithFee * reserveOut;
uint denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
거의 비슷하다. 굳.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "src/dex-2/Sniper.sol";
import {IUniswapV2Router02} from "src/interfaces/IUniswapV2.sol";
import "src/interfaces/IWETH9.sol";
import "src/utils/DummyERC20.sol";
/**
@dev run "forge test --fork-url $ETH_RPC_URL --fork-block-number 15969633 --match-contract DEX2 -vvv"
*/
contract TestDEX2 is Test {
address constant WETH_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
address constant UNISWAPV2_ROUTER_ADDRESS = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
address constant UNISWAPV2_FACTORY_ADDRESS = address(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
uint128 constant ETH_BALANCE = 300 ether;
uint128 constant INITIAL_MINT = 80000 ether;
uint128 constant INITIAL_LIQUIDITY = 10000 ether;
uint128 constant ETH_IN_LIQUIDITY = 50 ether;
uint128 constant ETH_TO_INVEST = 35 ether;
uint128 constant MIN_AMOUNT_OUT = 1750 ether;
address liquidityAdder = makeAddr("liquidityAdder");
address user = makeAddr("user");
IWETH9 weth = IWETH9(WETH_ADDRESS);
IUniswapV2Router02 router;
DummyERC20 preciousToken;
Sniper sniper;
function setUp() public {
vm.label(WETH_ADDRESS, "WETH");
vm.label(UNISWAPV2_ROUTER_ADDRESS, "UniswapV2Router02");
vm.label(UNISWAPV2_FACTORY_ADDRESS, "UniswapV2Factory");
// Set ETH balance
vm.deal(liquidityAdder, ETH_BALANCE);
vm.deal(user, ETH_BALANCE);
vm.startPrank(liquidityAdder);
// Deploy token
preciousToken = new DummyERC20("PreciousToken", "PRECIOUS", INITIAL_MINT);
// Load Uniswap Router contract
router = IUniswapV2Router02(UNISWAPV2_ROUTER_ADDRESS);
// Set the liquidity add operation deadline
uint deadline = block.timestamp + 10000;
// Deposit to WETH & approve router to spend tokens
weth.deposit{value: ETH_IN_LIQUIDITY}();
weth.approve(UNISWAPV2_ROUTER_ADDRESS, ETH_IN_LIQUIDITY);
preciousToken.approve(UNISWAPV2_ROUTER_ADDRESS, INITIAL_LIQUIDITY);
// Add the liquidity 10,000 PRECIOUS & 50 WETH
router.addLiquidity(
address(preciousToken),
WETH_ADDRESS,
INITIAL_LIQUIDITY,
ETH_IN_LIQUIDITY,
INITIAL_LIQUIDITY,
ETH_IN_LIQUIDITY,
liquidityAdder,
deadline
);
vm.stopPrank();
}
function test_Attack() public {
vm.startPrank(user);
// TODO: Deploy your smart contract 'sniper`
sniper = new Sniper(UNISWAPV2_FACTORY_ADDRESS);
// TODO: Sniper the tokens using your snipe function
// NOTE: Your rich friend is willing to invest 35 ETH in the project, and is willing to pay 0.02 WETH per PRECIOUS
// Which is 4x time more expensive than the initial liquidity price.
// You should retry 3 times to buy the token.
// Make sure to deposit to WETH and send the tokens to the sniper contract in advance
weth.deposit{value: ETH_TO_INVEST}();
weth.transfer(address(sniper), ETH_TO_INVEST);
sniper.snipe(address(weth), address(preciousToken), ETH_TO_INVEST, MIN_AMOUNT_OUT, 3);
vm.stopPrank();
/** SUCCESS CONDITIONS */
// Bot was able to snipe at least 4,000 precious tokens
// Bought at a price of ~0.00875 ETH per token (35 / 4000)
uint preciousBalance = preciousToken.balanceOf(user);
console.log("Sniped Balance");
console.log(preciousBalance);
assertEq(preciousBalance > 4000 ether, true);
}
}
세팅하는 부분이 조금 길어서 그렇지 특이사항은 없다.