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));
    }
}

簡單來說,要通過此挑戰,我們必須滿足三個條件:

  1. 部署了一個 usersDetectionBot 合約.

  2. 使得 cryptoVault.sweepToken(IERC20(instance.delegatedFrom())) 呼叫失敗.

  3. 並且同時使 instance.balanceOf(instance.cryptoVault()) > 0) 返回 true.

我們可以透過觀察 createInstance() 函數與 DoubleEntryPoint 合約知道此挑戰的運作方式。

我這邊長話短說簡單總結,此挑戰有四個合約:

  1. Forta 合約 - 作為 Detection Bots 的中央控制台,管理以下事情

    1. setDetectionBot() 負責協助新的 Detection Bot 的註冊。

    2. notify() 提供 DeFi 合約進行事件觸發 (例如: 發生了 transfer() 事件時, 自動執行 notify() 函數)。預期會呼叫 player 部署的 bot 的 handleTransaction() 函數。

    3. raiseAlert() 提供給 Detection Bot 做 alarm counter。

  2. CryptoVault 一個 Crypto Vault,預期持有 LGTDET 兩種 ERC20 token.

    1. setUnderlying() 設置 underlyingDET token address

    2. sweepToken() 轉移 token,它被設置成只能轉移 LGT token。

  3. LegacyToken (LGT) 一個 legacy ERC20 token

    1. transfer() 函數現在只會呼叫 DET.delegateTransfer(to, value, msg.sender)
  4. DoubleEntryPoint (DET) 一個 ERC20 token

    1. fortaNotify() 當被執行的時候,它會調用 Forta.notify()來向 player 的 Detection Bot 發出告警。如果 Detection Bot raised 了警報,則交易會失敗。

    2. 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 增加。

  1. 寫一個 IDetectionBot

  2. 完成 IDetectionBot.handleTransaction(user, msgData) 的實現

    1. 檢查 msgData 是否等於 abi.encodeWithSignature("delegateTransfer(address,uint256,address)", player, LGT.balanceOf(cryptoVault), address(cryptoVault))

    2. 如果等於,則向 Forta 合約 raise Alert

    3. 或者完全不檢查也可以啦 😂 畢竟 sweepToken() 函數只執行一次,只要有 raise Alert 就可以過關了

  3. 完成挑戰

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.

0
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. 🫣