Ethernaut题库闯关连载的第24篇
今天这篇是Ethernaut 题库闯关连载的第24篇,难度等级: 有点难。
这是系列的最后一篇,如果你跟随这个专栏,每一篇都认真思考, 相信你对 Solidity 安全有全新的认识。
本关有一个具有特殊功能的 CryptoVault
,即 sweepToken
功能。这是一个常用的功能,用于检索卡在合约中的代币并转移。CryptoVault
的操作有一个underlying
代币不能被转移,因为它是CryptoVault
的重要核心逻辑组件。任何其他代币都可以被转移。
underlying
是 DoubleEntryPoint
合约定义中实现的DET代币的实例,CryptoVault
持有100个。此外,CryptoVault
还持有100个LegacyToken LGT
。
在这一关中,我们需要找出CryptoVault
的错误所在,并保护它不被耗尽代币。
该合约的特点是Forta
合约,任何用户都可以注册自己的检测机器人
合约。Forta是一个去中心化的、基于社区的监测网络,以尽快检测DeFi、NFT、治理、跨链桥和其他Web3系统上的威胁和异常情况。我们的任务是实现一个 检测机器人
并在 Forta
合约中注册。该机器人的实现将需要提出正确的警报,以防止潜在的攻击或错误的利用。
合约源码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/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 {
require(address(usersDetectionBots[msg.sender]) == address(0), "DetectionBot already set");
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) 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)));
}
}
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) 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;
}
}
通过这一关,我们需要利用到双重进入点。
这个挑战似乎是OpenZeppelin和Forta(一个实时安全和运营监控)之间的一个联合。在我看来,这个挑战试图向我们解释应该如何整合Forta系统来监控合约。让我们看看它是如何进行的。
从挑战书的描述中,我们有两个代币:LegacyToken
,顾名思义是一个已经被 废弃
的代币(在现实生活中会发生这种情况吗),取而代之的是一个新的代币 DoubleEntryPoint
。
我们还有一个名为 CryptoVault
的金库,它有一些功能(与挑战的范围无关),并提供一个名为 sweepToken(IERC20 token)
的实用方法,允许任何人向 sweptTokensRecipient
(在部署时定义的地址)sweep
(转账)被意外发送到金库的代币。该函数内部的唯一检查是,你不能转移Vault的underlying
代币。
在部署时,我们以这种配置开始:
CryptoVault
持有100 DET (DoubleEntryToken
)CryptoVault
持有100 LGT (LegacyToken
)我们的目标是创建一个Forta DetectionBot,监测合约并防止外部攻击者耗尽CryptoVault
,使其耗尽不应耗尽的代币。
让我们回顾一下每个合约,看看是否能找到一些攻击的载体:
LegacyToken.sol
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);
}
}
}
这是一个继承自Ownable
的ERC20
代币。合约的 所有者
可以 铸造
新的代币,并通过调用 delegateToNewContract
更新 delegate
变量的值。
奇怪的部分是在transfer
函数里面,它已经覆盖了ERC20
标准提供的默认函数。
如果没有定义委托(address(delegate) == address(0)
),合约就使用ERC20
标准的默认逻辑;否则就执行return delegate.delegateTransfer(to, value, msg.sender)
。
在此案例中,delegate
就是DoubleEntryPoint
合约本身。这意味着什么呢?当你在LegacyToken
上执行转移时,实际上它是将操作转发给执行DoubleEntryPoint.delegateTransfer
。让我们切换到的DET token代码,看看发生了什么事
DoubleEntryPoint.sol
...如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!