1. 문제

이 레벨에서는 CryptoVault라는 특별한 기능을 가진 컨트랙트가 등장합니다.
그 중 핵심 기능은 sweepToken 함수인데, 이 함수는 일반적으로 컨트랙트에 “갇혀 있는” 토큰을 회수할 때 사용됩니다.

CryptoVault에는 기본(underlying) 토큰이라는 것이 있는데, 이 토큰은 CryptoVault의 핵심 로직에서 사용되므로 회수할 수 없게 설계되어 있습니다.
하지만, 이 기본 토큰이 아닌 다른 모든 토큰은 회수가 가능합니다.

현재 상황:
	•	기본 토큰은 DoubleEntryPoint 컨트랙트에서 구현된 DET 토큰이며, CryptoVault는 DET 토큰 100개를 보유하고 있습니다.
	•	CryptoVault는 LegacyToken (LGT) 100개도 보유하고 있습니다.

이번 레벨에서의 목표:
	1.	CryptoVault의 버그를 찾아내고,
	2.	그 버그를 악용해 토큰이 빠져나가지 않도록 보호해야 합니다.

⸻

CryptoVault에는 또 다른 중요한 요소로 Forta라는 컨트랙트가 있습니다.
	•	Forta는 사용자가 자신의 탐지 봇(detection bot) 컨트랙트를 등록할 수 있는 구조입니다.
	•	Forta는 탈중앙화된 커뮤니티 기반 모니터링 네트워크로, DeFi·NFT·거버넌스·브리지 등 다양한 Web3 시스템에서 발생할 수 있는 위협이나 이상 징후를 최대한 빨리 탐지하는 역할을 합니다.

당신의 역할:
	•	탐지 봇을 구현하고, Forta 컨트랙트에 등록하세요.
	•	봇이 올바르게 경고를 발생시켜야만 잠재적 공격이나 버그 악용을 방지할 수 있습니다.

힌트:
	•	Double Entry Point(이중 진입점)이 토큰 컨트랙트에서 어떻게 동작하는지 이해하는 것이 도움이 됩니다.
	
요약: IDetectionBot을 구현한 뒤 Forta 컨트랙트에 등록하여 자금이 유출되는 것을 막아라
// 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. 문제 분석

우리는 봇을 만들어 DET 자금이 유출되는 것을 막아야한다. 따라서 DET 를 인출할 수 있는 코드를 살펴보겠다.

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

LegacyToken contract는 transfer() 함수를 이용하여 delegate.delegateTransfer() 함수를 호출하여 transfer 작업을 진행하고 있다. 이떄 delegate는 DoubleEntryPoint 를 의미한다.

즉, DoubleEntryPoint contract 내부의 delegateTransfer() 함수를 호출한다.

function delegateTransfer(
    address to,
    uint256 value,
    address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
    _transfer(origSender, to, value);
    return true;
}

delegateTransfer() 함수를 살펴보면 _transfer 작업을 진행하고 있는데 이 함수에 접근하려면 onlyDelegateFrom, fortaNotify 두 가지 modifier를 통과해야 한다.

modifier onlyDelegateFrom() {
    require(msg.sender == delegatedFrom, "Not legacy contract");
    _;
}

onlyDelegateFrom() 는 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");
}