DoubleEntryPoint
1. 题目要求
1.1 此级别具有
CryptoVault
特殊功能,sweepToken
功能。这是用于检索卡在合约中的代币的常用函数。操作CryptoVault
使用underlying
无法清除的令牌,因为它是 . 的重要核心逻辑组件CryptoVault
。可以清除任何其他令牌。底层代币是合约定义中实施的 DET 代币的一个实例
DoubleEntryPoint
,并CryptoVault
持有 100 个单位。此外,CryptoVault
还拥有 100 个LegacyToken LGT
。在此级别中,您应该找出错误所在
CryptoVault
并防止它被耗尽令牌。该合约具有
Forta
合约功能,任何用户都可以注册自己的detection bot
合约。Forta 是一个去中心化的、基于社区的监控网络,用于尽快检测 DeFi、NFT、治理、桥梁和其他 Web3 系统上的威胁和异常。你的工作是实现一个detection bot
并在合约中注册它Forta
。机器人的实施将需要发出正确的警报,以防止潜在的攻击或漏洞利用。可能有帮助的事情:
- 双入口点如何为代币合约工作?
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/access/Ownable.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
interface DelegateERC20 {
function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}
interface IDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external;
}
interface IForta {
function setDetectionBot(address detectionBotAddress) external;
function notify(address user, bytes calldata msgData) external;
function raiseAlert(address user) external;
}
contract Forta is IForta {
mapping(address => IDetectionBot) public usersDetectionBots;
mapping(address => uint256) public botRaisedAlerts;
function setDetectionBot(address detectionBotAddress) external override {
usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
}
function notify(address user, bytes calldata msgData) external override {
if(address(usersDetectionBots[user]) == address(0)) return;
try usersDetectionBots[user].handleTransaction(user, msgData) {
return;
} catch {}
}
function raiseAlert(address user) external override {
if(address(usersDetectionBots[user]) != msg.sender) return;
botRaisedAlerts[msg.sender] += 1;
}
}
contract CryptoVault {
address public sweptTokensRecipient;
IERC20 public underlying;
constructor(address recipient) {
sweptTokensRecipient = recipient;
}
function setUnderlying(address latestToken) public {
require(address(underlying) == address(0), "Already set");
underlying = IERC20(latestToken);
}
/*
...
*/
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
}
contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}
function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
}
contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
address public cryptoVault;
address public player;
address public delegatedFrom;
Forta public forta;
constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) {
delegatedFrom = legacyToken;
forta = Forta(fortaAddress);
player = playerAddress;
cryptoVault = vaultAddress;
_mint(cryptoVault, 100 ether);
}
modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
}
2. 分析
这个挑战似乎是 OpenZeppelin 和 Forta 之间的合资企业,一个实时安全和操作监控。据我所知,试图向您解释您应该如何集成 Forta 系统来监控您的合同是一个挑战。让我们看看进展如何。
从挑战的描述(说实话不清楚)我们有两个令牌:
LegacyToken
顾名思义是一个已“弃用”的令牌(这在现实生活中发生过吗?)支持一个名为DoubleEntryPoint
.我们还有一个称为 Vault 的库
CryptoVault
,它具有一些功能(与挑战范围无关),并提供一种称为实用程序的方法,允许sweepToken(IERC20 token)
任何人“扫描”(转移)到sweptTokensRecipient
(部署时定义的地址)已发送的令牌不小心去了金库。该函数中唯一的检查是您不能扫除underlying
Vault 的令牌。在部署时,我们从这个配置开始:
CryptoVault
持有100 个 DET (DoubleEntryToken
)CryptoVault
持有100 LGT (LegacyToken
)
我们的目标是创建一个Forta DetectionBot来监控合约并防止外部攻击者耗尽本
CryptoVault
不应耗尽的代币。让我们回顾一下每个合同,看看我们是否能找到一些攻击媒介。
LegacyToken.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
DelegateERC20 public delegate;
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
delegate = newContract;
}
function transfer(address to, uint256 value) public override returns (bool) {
if (address(delegate) == address(0)) {
return super.transfer(to, value);
} else {
return delegate.delegateTransfer(to, value, msg.sender);
}
}
}它是一个
ERC20
继承自的令牌Ownable
。合约owner
的 可以通过调用mint
新代币和更新变量的值。delegate``delegateToNewContract
奇怪的部分是在
transfer
覆盖了标准提供的默认函数的函数中ERC20
。如果没有定义委托(
address(delegate) == address(0)
),则合约使用标准的默认逻辑ERC20
;否则执行return delegate.delegateTransfer(to, value, msg.sender)
。在这种情况下,
delegate
是DoubleEntryPoint
合同本身。这是什么意思?当您在现实中执行转移时,LegacyToken
它正在转发要执行的操作DoubleEntryPoint.delegateTransfer
。让我们切换到另一个令牌代码,看看发生了什么DoubleEntryPoint.sol
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
49contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
address public cryptoVault;
address public player;
address public delegatedFrom;
Forta public forta;
constructor(
address legacyToken,
address vaultAddress,
address fortaAddress,
address playerAddress
) public {
delegatedFrom = legacyToken;
forta = Forta(fortaAddress);
player = playerAddress;
cryptoVault = vaultAddress;
_mint(cryptoVault, 100 ether);
}
modifier onlyDelegateFrom() {
require(msg.sender == delegatedFrom, "Not legacy contract");
_;
}
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}
}合约是
ERC20
继承自 和 的DelegateERC20
普通代币Ownable
。DelegateERC20
是强制合约实现tokenfunction delegateTransfer(address to, uint256 value, address origSender)
需要的功能的接口LegacyToken
。有时
constructor
,设置一些状态变量并将100
令牌铸造到CryptoVault
.在进入
delegateTransfer
函数之前,让我们回顾一下fortaNotify
函数修饰符1
2
3
4
5
6
7
8
9
10
11
12
13
14
15modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
// Cache old number of bot alerts
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
// Notify Forta
forta.notify(player, msg.data);
// Continue execution
_;
// Check if alarms have been raised
if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}这个修改器的作用是触发 Forta 检测系统实现的一些逻辑。它在本地存储执行代码函数之前引发的警报数量,并将该数字与执行调用函数修饰符的函数主体之后引发的警报数量进行比较。
如果警报数量增加,交易将恢复并显示消息
"Alert has been triggered, reverting"
。让我们回顾一下调用
LegacyToken
“遗留”时令牌也使用的重要功能。LegacyToken.transfer
1
2
3
4
5
6
7
8function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}如果查看函数修饰符列表,您会看到
onlyDelegateFrom
只允许delegateFrom
调用此函数。在这种情况下,只LegacyToken
允许合约调用此函数,否则将允许任何人调用_transfer
(即低级 ERC20 传输)来自origSender
fortaNotify
是一个特殊的功能修饰符,可以触发一些特定的 Forta 逻辑,就像我们之前看到的那样
函数本身很简单,就是调用函数的ERC20内部实现
_transfer
。请记住,_transfer
只检查 thatto
和origSender
are notaddress(0)
以及origSender
有足够的令牌可以转移到to
(它还检查不足/溢出条件)但它不检查 thatorigSender
ismsg.sender
或消费者是否有足够的津贴。这就是为什么我们有修饰符onlyDelegateFrom
。CryptoVault.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22contract CryptoVault {
address public sweptTokensRecipient;
IERC20 public underlying;
constructor(address recipient) public {
sweptTokensRecipient = recipient;
}
function setUnderlying(address latestToken) public {
require(address(underlying) == address(0), "Already set");
underlying = IERC20(latestToken);
}
/*
...
*/
function sweepToken(IERC20 token) public {
require(token != underlying, "Can't transfer underlying token");
token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
}合约应实现普通加密 Vault 系统的逻辑。对于挑战的范围而言,这部分逻辑并不有趣。
由于任何金库也
CryptoVault
有一个基础令牌,在这种情况下是DoubleEntryPoint
.任何人都可以调用的函数
sweepToken
允许金库将任意token
(指定为输入参数)的整个金库余额转移到sweptTokensRecipient
. 收件人应该是安全的,因为它是由合同的部署者及时初始化的constructor
。从代码中可以看出,唯一完成的检查是防止 Vault 转移令牌
underlying
。通过部署 Forta DetectionBot 找到漏洞并阻止它
通过结合我们收集到的所有信息,您是否发现了我们可以利用的漏洞?回顾一下我们现有的知识:
CryptoVault
的underlying
令牌是DoubleEntryPoint
。该合约提供了一个sweepToken
在 Vault 中转移代币的方法,但它阻止了清除DoubleEntryPoint
代币(因为它是underlying
)DoubleEntryPoint
token 是一个 ERC20 令牌,它实现了一个delegateTransfer
只能由LegacyToken
令牌调用的自定义函数,并且由 Forta 通过执行函数修饰符来监控fortaNotify
。该函数允许委托人将一定数量的代币从origSpender
任意接收者转移LegacyToken
是已“弃用”的 ERC20 令牌。当transfer(address to, uint256 value)
函数被调用时DoubleEntryPoint
,(令牌的“新版本”)delegate.delegateTransfer(to, value, msg.sender)
被调用
问题在哪里?因为
LegacyToken.transfer
是“镜像”,DoubleEntryPoint.transfer
这意味着当您要求尝试转移 1 个时,LegacyToken
实际上您转移的是 1 个DoubleEntryPoint
代币(为了能够做到这一点,您的余额中必须同时拥有这两个代币)包含
CryptoVault
两个令牌中的 100 个,但sweepToken
仅阻止underlying
DoubleEntryPoint
.但是通过了解其工作原理,我们可以通过调用
LegacyToken
轻松扫除所有令牌。DoubleEntryPoint``CryptoVault.sweep(address(legacyTokenContract))
现在我们知道如何利用它,我们如何利用 Forta 集成来防止利用并恢复交易?我们可以构建一个扩展 Forta 的合约
IDetectionBot
并将其插入DoubleEntryPoint
. 通过这样做,我们应该能够在 VaultsweepToken
触发LegacyToken.transfer
将触发DoubleEntryPoint.delegateTransfer
将触发(在执行函数代码之前)函数fortaNotify
修饰符时防止利用。是的,我知道执行链很深,但请耐心等待,我们明白了!合约
IDetectionBot
接口只有一个函数签名function handleTransaction(address user, bytes calldata msgData) external;
,可以通过DoubleEntryPoint.delegateTransfer
这些参数直接调用forta.notify(player, msg.data)
。只有在这两个条件都为真时,我们才会在内部
DetectionBot
发出警报:- 原始发件人(正在呼叫的人
DoubleEntryPoint.delegateTransfer
)是CryptoVault
- 调用函数的签名(的前 4 个字节
calldata
)等于delegateTransfer
签名
让我们
origSender
从中提取值msgData
(请记住,在本例中,该参数值等于msg.data
)。如果您查看“特殊变量和函数”部分下的块和交易属性msg.data
的 Solidity 文档,您会看到这是一种代表完整 calldata 的bytes calldata
数据类型。这是什么意思?在这些字节中,您将同时拥有函数选择器(4 个字节)和函数有效负载。要提取参数,我们可以简单地使用
abi.decode
这样的(address to, uint256 value, address origSender) = abi.decode(msgData[4:], (address, uint256, address));
。一个重要的注意事项:我们假设在这些字节中,这些特定类型的三个值以这些特定顺序排列。我们正在做一个非常艰难的假设。这就是为什么我们需要将此信息与函数签名与强制执行这些类型和顺序要求的函数签名相匹配这一事实结合起来delegateTransfer
。1
msgData`第二部分非常简单,我们只需通过合并前 4 个字节来重建调用签名,`bytes memory callSig = abi.encodePacked(msgData[0], msgData[1], msgData[2], msgData[3]);`并将其与我们知道的正确签名进行比较`delegateTransfer`→`abi.encodeWithSignature("delegateTransfer(address,uint256,address)")
解决方案代码
让我们看看检测的整个代码
DetectionBot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19contract DetectionBot is IDetectionBot {
address private monitoredSource;
bytes private monitoredSig;
constructor(address _monitoredSource, bytes memory _monitoredSig) public {
monitoredSource = _monitoredSource;
monitoredSig = _monitoredSig;
}
function handleTransaction(address user, bytes calldata msgData) external override {
(address to, uint256 value, address origSender) = abi.decode(msgData[4:], (address, uint256, address));
bytes memory callSig = abi.encodePacked(msgData[0], msgData[1], msgData[2], msgData[3]);
if (origSender == monitoredSource && keccak256(callSig) == keccak256(monitoredSig)) {
IForta(msg.sender).raiseAlert(user);
}
}
}在构造函数内部,第一个参数将是我们要监视的源,在本例中是地址,
CryptoVault
第二个参数是我们打算监视的函数的签名,在本例中是abi.encodeWithSignature("delegateTransfer(address,uint256,address)")
。现在我们只需要部署传递正确参数的机器人并将机器人插入 Forta 系统并解决挑战。我们走吧!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function exploitLevel() internal override {
vm.startPrank(player, player);
// Create and deploy the `DetectionBot` with the correct constructor paramter
// The first one is the source we want to monitor
// The second one is the signature of the function we want to match
DetectionBot bot = new DetectionBot(
level.cryptoVault(),
abi.encodeWithSignature("delegateTransfer(address,uint256,address)")
);
// add the bot to the Forta network detection system that monitor the `DoubleEntryPoint` contract
level.forta().setDetectionBot(address(bot));
vm.stopPrank();
}