Ethernaut-26-DoubleEntryPoint
Challenge
This level features a CryptoVault
with special functionality, the sweepToken
function. This is a common function used to retrieve tokens stuck in a contract. The CryptoVault
operates with an underlying
token that can't be swept, as it is an important core logic component of the CryptoVault
. Any other tokens can be swept.
The underlying token is an instance of the DET token implemented in the DoubleEntryPoint
contract definition and the CryptoVault
holds 100 units of it. Additionally the CryptoVault
also holds 100 of LegacyToken LGT
.
In this level you should figure out where the bug is in CryptoVault
and protect it from being drained out of tokens.
The contract features a Forta
contract where any user can register its own detection bot
contract. Forta is a decentralized, community-based monitoring network to detect threats and anomalies on DeFi, NFT, governance, bridges and other Web3 systems as quickly as possible. Your job is to implement a detection bot
and register it in the Forta
contract. The bot's implementation will need to raise correct alerts to prevent potential attacks or bug exploits.
Things that might help:
- How does a double entry point work for a token contract?
// 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;
}
}
Solve
Uhhhhh….I forgot to save my English draft and now it's gone forever. 😭😭😭😭
Please forgive me for writing solve in my native language. 😰
老實說這一個挑戰讓我有點困惑,我不太曉得自己該做什麼來通過挑戰。於是我決定去檢查 validateInstance()
函數
// https://github.com/OpenZeppelin/ethernaut/blob/master/contracts/src/levels/DoubleEntryPointFactory.sol
function validateInstance(address payable _instance, address _player) public override returns (bool) {
DoubleEntryPoint instance = DoubleEntryPoint(_instance);
Forta forta = instance.forta();
// If user didn't set an DetectionBot, level failed.
address usersDetectionBot = address(forta.usersDetectionBots(_player));
if (usersDetectionBot == address(0)) return false;
address vault = instance.cryptoVault();
CryptoVault cryptoVault = CryptoVault(vault);
(bool ok, bytes memory data) = this.__trySweep(cryptoVault, instance);
require(!ok, "Sweep succeded");
bool swept = abi.decode(data, (bool));
return swept;
}
function __trySweep(CryptoVault cryptoVault, DoubleEntryPoint instance) external returns (bool, bytes memory) {
try cryptoVault.sweepToken(IERC20(instance.delegatedFrom())) {
return (true, abi.encode(false));
} catch {
return (false, abi.encode(instance.balanceOf(instance.cryptoVault()) > 0));
}
}
簡單來說,要通過此挑戰,我們必須滿足三個條件:
部署了一個
usersDetectionBot
合約.使得
cryptoVault.sweepToken(IERC20(instance.delegatedFrom()))
呼叫失敗.並且同時使
instance.balanceOf(instance.cryptoVault()) > 0)
返回 true.
我們可以透過觀察 createInstance()
函數與 DoubleEntryPoint
合約知道此挑戰的運作方式。
我這邊長話短說簡單總結,此挑戰有四個合約:
Forta
合約 - 作為 Detection Bots 的中央控制台,管理以下事情setDetectionBot()
負責協助新的 Detection Bot 的註冊。notify()
提供 DeFi 合約進行事件觸發 (例如: 發生了 transfer() 事件時, 自動執行notify()
函數)。預期會呼叫 player 部署的 bot 的handleTransaction()
函數。raiseAlert()
提供給 Detection Bot 做 alarm counter。
CryptoVault
一個 Crypto Vault,預期持有LGT
與DET
兩種 ERC20 token.setUnderlying()
設置underlying
為DET
token addresssweepToken()
轉移 token,它被設置成只能轉移LGT
token。
LegacyToken (LGT)
一個 legacy ERC20 tokentransfer()
函數現在只會呼叫DET.delegateTransfer(to, value, msg.sender)
DoubleEntryPoint (DET)
一個 ERC20 tokenfortaNotify()
當被執行的時候,它會調用Forta.notify()
來向 player 的 Detection Bot 發出告警。如果 Detection Bot raised 了警報,則交易會失敗。delegateTransfer()
會執行fortaNotify()
,如果沒有警報被觸發,則會正常的轉移DET
token
接下來我們來追蹤一個正常的 cryptoVault.sweepToken(IERC20(instance.delegatedFrom()))
執行流程會是長怎樣的:
cryptoVault.sweepToken(IERC20(LGT));
|_ LGT.transfer(player, LGT.balanceOf(cryptoVault));
|_ DET.delegateTransfer(player, LGT.balanceOf(cryptoVault), address(cryptoVault));
|_ fortaNotify()
|_ address detectionBot = PLAYER_DEPLOYED_DETECTION_BOT;
|_ uint256 previousValue = forta.botRaisedAlerts(PLAYER_DEPLOYED_DETECTION_BOT);
|_ forta.notify(player, msg.data);
|_ PLAYER_DEPLOYED_DETECTION_BOT.handleTransaction(user=player, msgData=abi.encodeWithSignature("delegateTransfer(address,uint256,address)", player, LGT.balanceOf(cryptoVault), address(cryptoVault)))
|_ _;
|_ if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
我們的目標是要使 revert("Alert has been triggered, reverting")
被觸發,從而使得 cryptoVault.sweepToken(IERC20(instance.delegatedFrom()))
執行失敗。
所以破關最簡單的方式是寫一個 Detection Bot、完成 handleTransaction()
的實現,然後在滿足特定條件的情況下使 botRaisedAlerts
增加。
寫一個
IDetectionBot
完成
IDetectionBot.handleTransaction(user, msgData)
的實現檢查
msgData
是否等於abi.encodeWithSignature("delegateTransfer(address,uint256,address)", player, LGT.balanceOf(cryptoVault), address(cryptoVault))
如果等於,則向
Forta
合約 raise Alert或者完全不檢查也可以啦 😂 畢竟
sweepToken()
函數只執行一次,只要有 raise Alert 就可以過關了
完成挑戰
Full solution code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
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;
}
abstract contract Forta is IForta {}
interface IDoubleEntryPoint{
function cryptoVault() external returns (address);
function forta() external returns (Forta);
}
contract Solver is Script {
address doubleEntrypoint = vm.envAddress("DOUBLE_ENTRYPOINT_INSTANCE");
function setUp() public {}
function run() public {
vm.startBroadcast(vm.envUint("PRIV_KEY"));
address cryptoVault = IDoubleEntryPoint(doubleEntrypoint).cryptoVault();
MyDetectionBot bot = new MyDetectionBot();
IDoubleEntryPoint(doubleEntrypoint).forta().setDetectionBot(address(bot));
vm.stopBroadcast();
}
}
contract MyDetectionBot {
function handleTransaction(address user, bytes calldata msgData) external {
IForta(msg.sender).raiseAlert(user);
}
}
Potential Patches
None, this level is just designed for fun.
Subscribe to my newsletter
Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
whiteberets[.]eth
whiteberets[.]eth
Please don't OSINT me, I'd be shy. 🫣