Dex
1. 题目要求
1.1
此题目的目标是让您破解下面的基本合约并通过价格操纵窃取资金。
一开始您可以得到10个token1和token2。合约以每个代币100个开始。
如果您设法从合约中取出两个代币中的至少一个,并让合约得到一个的“坏”的token价格,您将在此级别上取得成功。
注意: 通常,当您使用ERC20代币进行交换时,您必须approve合约才能为您使用代币。为了与题目的语法保持一致,我们刚刚向合约本身添加了approve方法。因此,请随意使用 contract.approve(contract.address,
) 而不是直接调用代币,它会自动批准将两个代币花费所需的金额。 请忽略SwappableToken合约。 可能有帮助的注意点:
- 代币的价格是如何计算的?
- approve方法如何工作?
- 您如何批准ERC20 的交易?
1.2 题目代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56// 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);
}
}
2. 分析
tips: 参考博客
2.1 分析代码可知:这是一个简单的
ERC20
令牌,它向铸造一个initialSupply
(指定为 的输入)并覆盖了函数以防止地址能够批准任何令牌。2.2 分析 Dex.sol 合约可知,它允许
owner
Dex 提供一对代币的流动性token1
,并且token2
在最终用户交换这些代币时不收取任何费用。最终用户将使用 Dex 来swap
(出售)特定数量的一种代币,以取回swapAmount
(取决于 Dex 的代币价格)另一种代币。function setTokens(address _token1, address _token2) public onlyOwner
1
2
3
4function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}该功能允许Dex平台的所有者设置
token1
和的地址token2
。该函数正确检查只有owner
Dex 的 才能调用此函数。owner
当已经提供这些代币的供应时,防止更改这些地址也是有意义的(否则旧代币将永远卡在合约中)。function approve(address spender, uint256 amount) public
1
2
3
4function approve(address spender, uint256 amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}这是一个更实用的功能,允许最终用户批准
spender
管理amount
两个令牌中的一个。这里没有什么奇怪的。您可以通过直接调用传递相同参数的token1
和函数来实现相同的结果,正如我所说的,它只是一个实用函数,可以让最终用户的生活更轻松。token2
approve
function balanceOf(address token, address account) public view returns (uint256)
1
2
3function balanceOf(address token, address account) public view returns (uint256) {
return IERC20(token).balanceOf(account);
}获取特定代币地址的用户余额的简单实用函数。
function swap(address from, address to, uint256 amount) public
1
2
3
4
5
6
7
8
9
10
11
12function 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);
}这是负责将一个代币与另一个代币交换(出售/购买)的功能。您看到的第一个
require
,检查您是否只能交换token1
,token2
反之亦然。之后,Dex 计算掉期价格。对于给定
amount
的一个令牌,用户取回了多少其他令牌?然后它执行所有需要的传输
amount
将出售的代币从用户转移到 Dex 合约- 批准Dex管理
swapAmount
用户购买的代币 swapAmount
从 Dex 向用户转移金额
当且仅当两者
token1
都是代币标准token2
的良好实施时,才不需要对这些金额进行检查ERC20
。当前的 Dex 正在使用 OpenZeppelin ERC20 实现的两种代币,因此如果 Dex 或用户的余额中没有足够数量的代币来执行转账,交易将自动恢复function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256)
这是整个合约中最核心也是最重要的功能。此功能负责计算掉期价格。执行 的
tokenX
交换操作时,用户获得多少令牌?tokenY
Dex 内部的当前实现是使用代币余额来计算价格,并因此计算用户将收到的代币数量。
为什么这是个问题?使用余额作为计算价格的一个因素将使您的合约热衷于称为“价格操纵”的攻击,不幸的是(但不仅与这个简单的余额案例有关)它并不少见。
用于计算用户因交换操作而收到的代币数量的公式如下
((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)))
这个公式告诉你当你发送代币
to
时你会得到多少代币。较低的是(与 的余额相比)的余额,较高的是 的金额。amount``from``from``to``to
该 Dex 不使用外部Oracle(如Chainlink)或Uniswap TWAP(时间加权平均价格)来计算掉期价格。相反,它使用令牌的余额来计算它,我们可以利用它。
在 Solidity 中,有一个称为“舍入误差”的已知问题。这个问题是由所有整数除法向下舍入到最接近的整数这一事实引起的。这意味着如果你执行
5/2
结果将不是2.5
but2
。举个例子,如果我们卖掉 1,
token1
但token2*amount < token1
我们会拿回0token2
!基本上我们会出售代币以获得零回报!2.3 参考视频 编写攻击合约
1 | contract Hack { |
