티스토리 뷰

Solidity/Hacking

[Ethernaut] 22. Dex

piatoss 2024. 2. 3. 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

The goal of this level is for you to hack the basic [DEX](https://en.wikipedia.org/wiki/Decentralized_exchange)contract below and steal the funds by price manipulation.

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

You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a "bad" price of the assets.

Quick note

Normally, when you make a swap with an ERC20 token, you have to `approve` the contract to spend your tokens for you. To keep with the syntax of the game, we've just added the `approve` method to the contract itself. So feel free to use `contract.approve(contract.address, <uint amount>)` instead of calling the tokens directly, and it will automatically approve spending the two tokens by the desired amount. Feel free to ignore the `SwappableToken` contract otherwise.

  Things that might help:

- How is the price of the token calculated?
- How does the `swap` method work?
- How do you `approve` a transaction of an ERC20?
- Theres more than one way to interact with a contract!
- Remix might help
- What does "At Address" do?

// 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 Dex is Ownable {
  address public token1;
  address public token2;
  constructor() {}

  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) 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. 풀이

 컨트랙트 작성 없이 콘솔창을 사용해서 swap 함수를 반복해서 호출해야 하므로 먼저 아주 큰 수로 approve 함수를 호출하여 번거로움을 덜어줍니다.

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

 인스턴스가 가진 모든 token1을 탈취했습니다. 반대로 token2 10개를 token1로 먼저 교환한 경우는 인스턴스가 가진 모든 token2를 탈취할 수 있습니다.

 

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


3. 가격 결정 알고리즘이 잘못됐다

 대부분의 탈중앙화 거래소는 유니스왑으로부터 기인하여 AMM(Automated Market Maker) 방식을 사용하는데, 이 방식을 사용할 때는 가격 결정 알고리즘이 굉장히 중요합니다. 이 문제의 컨트랙트의 경우는 다음 함수를 통해 토큰의 가격을 구합니다.

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

 문제는 가격만 구하는 것이 아니라 amount를 곱해서 그만큼의 to 토큰으로 교환을 해주기 때문에 한 번에 많은 from 토큰을 교환하면 그만큼 많은 양의 to 토큰을 가져갈 수 있습니다. 문제에서도 이를 활용해 컨트랙트의 자금을 탈취할 수 있었습니다.

 

 문제가 뭔지는 얼추 알겠는데 사실 DEX에 익숙하지 않은 경우에는 어떤 알고리즘이 필요한지 바로 알아맞히기란 여간 쉬운 일은 아닙니다. 저도 그러한 이유로 이 문제를 처음 마주하고 DEX에 대한 공부의 필요성을 느껴서 유니스왑 V2 컨트랙트를 살펴보고 있습니다. 유니스왑에서는 대표적으로 CPMM이라는 가격 결정 알고리즘을 사용합니다. 이와 관련해서는 아래 게시글에서 추가적인 내용을 살펴보실 수 있습니다.

 

[Uniswap] V2 Core - UniswapV2Pair

⛓️ 시리즈 2024.01.30 - [Solidity/DeFi] - [Uniswap] V2 Core - UniswapV2ERC20 2024.01.31 - [Solidity/DeFi] - [Uniswap] V2 Core - UniswapV2Factory 🦄 IUniswapV2Pair.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; // 유동성 풀

piatoss3612.tistory.com

 

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

[Ethernaut] 27. Good Samaritan  (1) 2024.02.05
[Ethernaut] 23. Dex Two  (0) 2024.02.04
[Ethernaut] 21. Shop  (0) 2024.02.02
[Ethernaut] 20. Denial  (0) 2024.02.01
[Ethernaut] 19. Alien Codex  (1) 2024.01.31
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
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
Total
Today
Yesterday
글 보관함