DEX 스마트 컨트랙트를 해킹하여 가격 조작을 통해 자금을 탈취하라
나는 게임 시작 시 Token1과 Token2를 각각 10개씩 가지고 있다.
DEX 컨트랙트는 Token1과 Token2를 각각 100개씩 보유한 상태로 시작한다.
이 레벨을 성공하려면, DEX가 가진 Token1 또는 Token2 중 최소 하나를 완전히 빼앗고, 컨트랙트가 잘못된 자산 가격(“bad” price)을 보여주도록 만들면 됩니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";
import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.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, uint256 amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint256 amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint256 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, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint256 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 (uint256) {
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);
}
}
function swap(address from, address to, uint256 amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint256 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);
}
swap() 함수를 살펴보면 dex와 나의 지갑에서 token을 getSwapPrice() 값에서 return해주는 값으로 스왑해주는 역할을 하는 것을 확인할 수 있다.
function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
해당 코드를 보면 (10 * DEX의 token1 잔고) / DEX의 token2 잔고의 상대적인 값을 계산해 반환해주는 것을 확인할 수 있다.
token1, token2의 비율에 따라 return 값이 달라지는 것을 보아 이를 이용하여 swap 함수를 여러번 왓다갔다 호출하면 dex에 있는 token을 가져올 수 있을 것 같다.
나의 token1 10개를 DEX에게 주고, DEX의 token2 10개를 나에게 전송한다