티스토리 뷰
1. 문제
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이라는 가격 결정 알고리즘을 사용합니다. 이와 관련해서는 아래 게시글에서 추가적인 내용을 살펴보실 수 있습니다.
'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 |