유니스왑V2 컨트랙트를 참고해서 빈칸들을 채워보자.
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../interfaces/IUniswapV2.sol";
interface IWETH is IERC20 {
function deposit() external payable;
function transfer(address to, uint value) external returns (bool);
function withdraw(uint) external;
}
/**
* @title Chocolate
* @author JohnnyTime (https://smartcontractshacking.com)
*/
contract Chocolate is ERC20, Ownable {
using Address for address payable;
IUniswapV2Router02 public uniswapV2Router;
address public weth;
address public uniswapV2Pair;
constructor(uint256 _initialMint) ERC20("Chocolate Token", "Choc") {
// TODO: Mint tokens to owner
// TODO: SET Uniswap Router Contract
// TODO: Set WETH (get it from the router)
// TODO: Create a uniswap Pair with WETH, and store it in the contract
}
/*
@dev An admin function to add liquidity of chocolate with WETH
@dev payable, received Native ETH and converts it to WETH
@dev lp tokens are sent to contract owner
*/
function addChocolateLiquidity(uint256 _tokenAmount) external payable onlyOwner {
// TODO: Transfer the tokens from the sender to the contract
// Sender should approve the contract spending the chocolate tokens
// TODO: Convert ETH to WETH
// TODO: Approve the router to spend the tokens
// TODO: Add the liquidity, using the router, send lp tokens to the contract owner
}
/*
@dev An admin function to remove liquidity of chocolate with WETH
@dev received `_lpTokensToRemove`, removes the liquidity
@dev and sends the tokens to the contract owner
*/
function removeChocolateLiquidity(uint256 _lpTokensToRemove) external onlyOwner {
// TODO: Transfer the lp tokens from the sender to the contract
// Sender should approve token spending for the contract
// TODO: Approve the router to spend the tokens
// TODO: Remove the liquiduity using the router, send tokens to the owner
}
/*
@dev User facing helper function to swap chocolate to WETH and ETH to chocolate
@dev received `_lpTokensToRemove`, removes the liquidity
@dev and sends the tokens to the contract user that swapped
*/
function swapChocolates(address _tokenIn, uint256 _amountIn) public payable {
// TODO: Implement a dynamic function to swap Chocolate to ETH or ETH to Chocolate
if(_tokenIn == address(this)) {
// TODO: Revert if the user sent ETH
// TODO: Set the path array
// TODO: Transfer the chocolate tokens from the sender to this contract
// TODO: Approve the router to spend the chocolate tokens
} else if(_tokenIn == weth) {
// TODO: Make sure msg.value equals _amountIn
// TODO: Convert ETH to WETH
// TODO: Set the path array
// TODO: Approve the router to spend the WETH
} else {
revert("wrong token");
}
// TODO: Execute the swap, send the tokens (chocolate / weth) directly to the user (msg.sender)
}
}
constructor(uint256 _initialMint) ERC20("Chocolate Token", "Choc") {
// TODO: Mint tokens to owner
// TODO: SET Uniswap Router Contract
// TODO: Set WETH (get it from the router)
// TODO: Create a uniswap Pair with WETH, and store it in the contract
}
constructor(uint256 _initialMint) ERC20("Chocolate Token", "Choc") {
// TODO: Mint tokens to owner
_mint(owner(), _initialMint);
// TODO: SET Uniswap Router Contract
uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
// TODO: Set WETH (get it from the router)
weth = uniswapV2Router.WETH();
// TODO: Create a uniswap Pair with WETH, and store it in the contract
uniswapV2Pair = IUniswapV2Factory(uniswapV2Router.factory()).createPair(address(this), weth);
}
문제에서 Router 주소를 제공해줘서 인터페이스를 적용해서 가져온 것 외에 특이한 점은 없다.
function addChocolateLiquidity(uint256 _tokenAmount) external payable onlyOwner {
// TODO: Transfer the tokens from the sender to the contract
// Sender should approve the contract spending the chocolate tokens
// TODO: Convert ETH to WETH
// TODO: Approve the router to spend the tokens
// TODO: Add the liquidity, using the router, send lp tokens to the contract owner
}
function addChocolateLiquidity(uint256 _tokenAmount) external payable onlyOwner {
// TODO: Transfer the tokens from the sender to the contract
IERC20(address(this)).transferFrom(msg.sender, uniswapV2Pair ,_tokenAmount);
// Sender should approve the contract spending the chocolate tokens
IERC20(address(this)).approve(address(this), _tokenAmount);
// TODO: Convert ETH to WETH
IWETH(weth).deposit{value: msg.value}();
// TODO: Approve the router to spend the tokens
IERC20(address(this)).approve(address(uniswapV2Router), _tokenAmount);
// TODO: Add the liquidity, using the router, send lp tokens to the contract owner
uniswapV2Router.addLiquidity(address(this), weth, _tokenAmount, msg.value, 1, 1, owner(), block.timestamp);
}
function addChocolateLiquidity(uint256 _tokenAmount) external payable onlyOwner {
// TODO: Transfer the tokens from the sender to the contract
// Sender should approve the contract spending the chocolate tokens
_transfer(msg.sender, address(this), _tokenAmount);
// TODO: Convert ETH to WETH
IWETH(weth).deposit{value: msg.value}();
// TODO: Approve the router to spend the tokens
IWETH(weth).approve(address(uniswapV2Router), msg.value);
_approve(address(this), address(uniswapV2Router), _tokenAmount);
// TODO: Add the liquidity, using the router, send lp tokens to the contract owner
uniswapV2Router.addLiquidity(address(this), weth, _tokenAmount, msg.value, 1, 1, owner(), block.timestamp);
}
우선 주석이 나눠져있는지 알았는데 하나였었고, ERC20
을 상속했기 때문에 _transfer
와 _approve
를 바로 사용할 수 있었다. 한 가지 잘못 생각했던 점은 2 번째 라인에서 컨트랙트 스스로를 approve
의 spender로 설정했다는 것이다. approve
는 내가 갖고 있는 토큰을 다른 컨트랙트 혹은 다른 사용자에게 권한을 넘길 때 사용하는 함수이기 때문에 스스로를 spender로 설정할 일이 없다.
function removeChocolateLiquidity(uint256 _lpTokensToRemove) external onlyOwner {
// TODO: Transfer the lp tokens from the sender to the contract
// Sender should approve token spending for the contract
// TODO: Approve the router to spend the tokens
// TODO: Remove the liquiduity using the router, send tokens to the owner
}
function removeChocolateLiquidity(uint256 _lpTokensToRemove) external onlyOwner {
// TODO: Transfer the lp tokens from the sender to the contract
// Sender should approve token spending for the contract
IUniswapV2Pair(uniswapV2Pair).transferFrom(msg.sender, address(this), _lpTokensToRemove);
// TODO: Approve the router to spend the tokens
IUniswapV2Pair(uniswapV2Pair).approve(address(uniswapV2Router), _lpTokensToRemove);
// TODO: Remove the liquiduity using the router, send tokens to the owner
uniswapV2Router.removeLiquidity(address(this), weth, _lpTokensToRemove, 1, 1, owner(), block.timestamp);
}
function removeChocolateLiquidity(uint256 _lpTokensToRemove) external onlyOwner {
// TODO: Transfer the lp tokens from the sender to the contract
// Sender should approve token spending for the contract
IERC20(uniswapV2Pair).transferFrom(owner(), address(this), _lpTokensToRemove);
// TODO: Approve the router to spend the tokens
IERC20(uniswapV2Pair).approve(address(uniswapV2Router), _lpTokensToRemove);
// TODO: Remove the liquiduity using the router, send tokens to the owner
uniswapV2Router.removeLiquidity(address(this), weth, _lpTokensToRemove, 1, 1, owner(), block.timestamp);
}
IUniswapV2Pair
에도 transferFrom과 approve가 있는데 IERC20
으로 해도 상관없을 것 같다. 이건 어떤 게 맞는지 정확히 모르겠다.
function swapChocolates(address _tokenIn, uint256 _amountIn) public payable {
// TODO: Implement a dynamic function to swap Chocolate to ETH or ETH to Chocolate
if(_tokenIn == address(this)) {
// TODO: Revert if the user sent ETH
// TODO: Set the path array
// TODO: Transfer the chocolate tokens from the sender to this contract
// TODO: Approve the router to spend the chocolate tokens
} else if(_tokenIn == weth) {
// TODO: Make sure msg.value equals _amountIn
// TODO: Convert ETH to WETH
// TODO: Set the path array
// TODO: Approve the router to spend the WETH
} else {
revert("wrong token");
}
// TODO: Execute the swap, send the tokens (chocolate / weth) directly to the user (msg.sender)
}
function swapChocolates(address _tokenIn, uint256 _amountIn) public payable {
// TODO: Implement a dynamic function to swap Chocolate to ETH or ETH to Chocolate
address[] memory path;
if(_tokenIn == address(this)) {
// TODO: Revert if the user sent ETH
require(_tokenIn != address(weth), "It should be ETH");
// TODO: Set the path array
path[0] = _tokenIn;
path[1] = weth;
// TODO: Transfer the chocolate tokens from the sender to this contract
IERC20(address(this)).transferFrom(msg.sender, address(this), _amountIn);
// TODO: Approve the router to spend the chocolate tokens
IERC20(address(this)).approve(address(uniswapV2Router), _amountIn);
} else if(_tokenIn == weth) {
// TODO: Make sure msg.value equals _amountIn
require(msg.value == _amountIn);
// TODO: Convert ETH to WETH
IWETH(weth).deposit{value : msg.value}();
// TODO: Set the path array
path[0] = weth;
path[1] = _tokenIn;
// TODO: Approve the router to spend the WETH
IERC20(weth).approve(address(uniswapV2Router), _amountIn);
} else {
revert("wrong token");
}
// TODO: Execute the swap, send the tokens (chocolate / weth) directly to the user (msg.sender)
uniswapV2Router.swapExactTokensForTokens(_amountIn, 1, path, msg.sender, 0);
}
두 번째 else if
부분에서 weth가 들어왔는데 왜 이더를 weth로 또 바꾸는지 이해가 안 갔었다. 그런데 곰곰이 생각해보니 이미 weth가 들어온 게 아니라 "weth를 chocolate 토큰으로 바꿀거예요"라고 요청하는 것이기 때문에 아직 weth가 안 들어왔다고 볼 수 있다.
function swapChocolates(address _tokenIn, uint256 _amountIn) public payable {
// TODO: Implement a dynamic function to swap Chocolate to ETH or ETH to Chocolate
address[] memory path = new address[](2);
if (_tokenIn == address(this)) {
// TODO: Revert if the user sent ETH
require(msg.value == 0, "Don't send ETH!!!");
// TODO: Set the path array
path[0] = address(this);
path[1] = weth;
// TODO: Transfer the chocolate tokens from the sender to this contract
_transfer(msg.sender, address(this), _amountIn);
// TODO: Approve the router to spend the chocolate tokens
_approve(address(this), address(uniswapV2Router), _amountIn);
} else if (_tokenIn == weth) {
// TODO: Convert ETH to WETH
IWETH(weth).deposit{value: msg.value}();
// TODO: Set the path array
path[0] = weth;
path[1] = address(this);
// TODO: Approve the router to spend the WETH
IWETH(weth).approve(address(uniswapV2Router), msg.value);
} else {
revert("wrong token");
}
// TODO: Execute the swap, send the tokens (chocolate / weth) directly to the user (msg.sender)
uniswapV2Router.swapExactTokensForTokens(_amountIn, 0, path, msg.sender, block.timestamp);
}
나는 path
를 선언만 했었다. 이렇게 선언만 하고 initialize 하지 않은 array는 나중에 사용할 때 out-of-bounds error
가 발생할 수 있다. 따라서 모범답안처럼 크기가 정해진 array를 initialize 해주는 게 좋다.
모범답안 마지막 부분에서 swapExactTokensForTokens
의 두 번째 파라미터를 0으로 했는데, 최솟값을 0으로 한 이유는 조금 더 알아봐야겠다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "src/dex-1/Chocolate.sol";
import {IUniswapV2Pair} from "src/interfaces/IUniswapV2.sol";
/**
@dev run "forge test --fork-url $ETH_RPC_URL --fork-block-number 15969633 --match-contract DEX1"
*/
contract TestDEX1 is Test {
address deployer;
address user;
Chocolate chocolate;
address pair;
address weth;
uint256 initial_deployer_LP;
function setUp() public {
deployer = address(1);
user = address(2);
vm.deal(deployer, 100 ether);
vm.deal(user, 100 ether);
vm.prank(deployer);
chocolate = new Chocolate(1000000);
console.log("chocolate pair address :", address(chocolate));
pair = chocolate.uniswapV2Pair();
weth = chocolate.weth();
}
function test() public {
vm.startPrank(deployer);
chocolate.addChocolateLiquidity{value: 100 ether}(1000000);
vm.stopPrank();
console.log("deployer's LP token is :", IERC20(address(pair)).balanceOf(deployer));
initial_deployer_LP = IERC20(address(pair)).balanceOf(deployer);
assertEq(IERC20(address(chocolate)).balanceOf(user), 0);
vm.startPrank(user);
chocolate.swapChocolates{value: 10 ether}(weth, 10 ether);
vm.stopPrank();
assertEq(IERC20(address(chocolate)).balanceOf(user), 100_000);
vm.startPrank(user);
chocolate.swapChocolates(address(chocolate), 100);
vm.stopPrank();
assertEq(IERC20(address(chocolate)).balanceOf(user), 100_000 - 100);
vm.startPrank(deployer);
uint256 halfLP = initial_deployer_LP / 2;
IERC20(address(pair)).approve(address(chocolate), halfLP);
chocolate.removeChocolateLiquidity(halfLP);
vm.stopPrank();
console.log("deployer's LP token is :", IERC20(address(pair)).balanceOf(deployer));
assertEq(IERC20(address(chocolate)).balanceOf(deployer) >= 450000, true);
assertEq(IERC20(address(weth)).balanceOf(deployer) >= 100 ether / 2, true);
}
}
contract TestDEX1 is Test {
Chocolate chocolate;
IUniswapV2Pair pair;
address constant WETH_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
address constant RICH_SIGNER = address(0x8EB8a3b98659Cce290402893d0123abb75E3ab28);
uint128 constant ETH_BALANCE = 300 ether;
uint128 constant INITIAL_MINT = 1000000 ether;
uint128 constant INITIAL_LIQUIDITY = 100000 ether;
uint128 constant ETH_IN_LIQUIDITY = 100 ether;
uint128 constant TEN_ETH = 10 ether;
uint128 constant HUNDRED_CHOCOLATES = 100 ether;
address deployer = makeAddr("deployer");
address user = makeAddr("user");
IERC20 weth = IERC20(WETH_ADDRESS);
function setUp() public {
vm.label(WETH_ADDRESS, "WETH");
vm.label(RICH_SIGNER, "RICH_SIGNER");
vm.deal(user, 100 ether);
address richSigner = RICH_SIGNER;
// Send ETH from rich signer to our deployer
vm.prank(richSigner);
(bool success, ) = deployer.call{value: ETH_BALANCE}("");
require(success, "Transfer Failed!!!");
}
function test_Attack() public {
/************************Deployment************************/
// TODO: Deploy your smart contract to `chocolate`, mint 1,000,000 tokens to deployer
vm.prank(deployer);
chocolate = new Chocolate(INITIAL_MINT);
// TODO: Print newly created pair address and store pair contract to `this.pair`
address pairAddress = chocolate.uniswapV2Pair();
//console.log(pairAddress);
pair = IUniswapV2Pair(pairAddress);
/************************Deployer add liquidity tests************************/
// TODO: Add liquidity of 100,000 tokens and 100 ETH (1 token = 0.001 ETH)
vm.startPrank(deployer);
chocolate.approve(address(chocolate), INITIAL_LIQUIDITY);
chocolate.addChocolateLiquidity{value: ETH_IN_LIQUIDITY}(INITIAL_LIQUIDITY);
vm.stopPrank();
// TODO: Print the amount of LP tokens that the deployer owns
uint256 lpBalance = pair.balanceOf(deployer);
console.log(lpBalance);
/************************User swap tests************************/
uint256 userChocolateBalance = chocolate.balanceOf(user);
uint256 userWETHBalance = weth.balanceOf(user);
// TODO: From user: Swap 10 ETH to Chocolate
vm.prank(user);
chocolate.swapChocolates{value: TEN_ETH}(address(weth), TEN_ETH);
// TODO: Make sure user received the chocolates (greater amount than before)
assertEq(chocolate.balanceOf(user) > userChocolateBalance, true);
// TODO: From user: Swap 100 Chocolates to ETH
vm.startPrank(user);
chocolate.approve(address(chocolate), HUNDRED_CHOCOLATES);
chocolate.swapChocolates(address(chocolate), HUNDRED_CHOCOLATES);
vm.stopPrank();
// TODO: Make sure user received the WETH (greater amount than before)
assertEq(weth.balanceOf(user) > userWETHBalance, true);
/************************Deployer remove liquidity tests************************/
uint256 deployerChocolateBalance = chocolate.balanceOf(deployer);
uint256 deployerWETHBalance = weth.balanceOf(deployer);
// TODO: Remove 50% of deployer's liquidity
vm.startPrank(deployer);
pair.approve(address(chocolate), lpBalance / 2);
chocolate.removeChocolateLiquidity(lpBalance / 2);
vm.stopPrank();
// TODO: Make sure deployer owns 50% of the LP tokens (leftovers)
assertEq(pair.balanceOf(deployer), lpBalance / 2);
// TODO: Make sure deployer got chocolate and weth back (greater amount than before)
assertEq(weth.balanceOf(deployer) > deployerWETHBalance, true);
}
}
contant
로 이더 수를 정해놓고 시작하지 않아서 잘못된 숫자를 사용했다. 다음엔 꼭 테스트 코드 작성할 때 숫자를 먼저 표기하고 시작하자.vm.label()
을 사용하면 테스트 결과에서 주소 대신 표시한 라벨로 나와서 보기 편하다.1000000 ether
를 해줬어야 한다.)
swapExactTokensForTokens의 두번째 파라미터 (amountOutMin)의 경우 슬리피지로 인해 해당 Min값 이하로 토콘이 out되게 되면 revert를 내게끔 되어있어 0을 넣어줍니다! ㅎㅎ