# The Ethernaut CTF Solutions | 26 - Double Entry Point

## Goals

[![](https://github.com/Pedrojok01/Ethernaut-Solutions/raw/main/assets/requirements/26-doubleEntry-requirements.webp align="left")](https://github.com/Pedrojok01/Ethernaut-Solutions/blob/main/assets/requirements/26-doubleEntry-requirements.webp)

## The Contract

A pretty long code base this time, which is a good exercise to divide the task into smaller parts so we don't get overwhelmed. This is closer to a proper security review with multiple contracts and interactions between them. It also introduces us to the Forta bots, which are an important tool to increase the safety of the overall ecosystem.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// import "openzeppelin-contracts-08/access/Ownable.sol";
// import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import {Ownable} from "../helpers/Ownable-05.sol";
import {ERC20} from "@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 {
        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;
    }
}
```

## The hack

The goal of the DoubleEntryPoint level is to find the bug in `CryptoVault` contract and to protect it from being drained by implementing a Forta bot.

### Find the bug

The code given is a bit complex, so let's break it done for clarity. We have 4 contracts to work with:

1. `Forta` - Related to the AlertBot we need to set up.
    
2. `CryptoVault` - The contract we need to protect. Handles the `underlying` token.
    
3. `LegacyToken` - A modified ERC20 token (LGT).
    
4. `DoubleEntryPoint` - The instance we are given and the so-called `underlying` ERC20 token (DET).
    

We know that the `CryptoVault` is vulnerable. It contains 100 LGT and 100 DET.

Let's forget the `Forta` contract for now. We will come back to it later.

Hard to say what's wrong with the `CryptoVaults` contract at first glance. So let's focus on the token contracts.

```solidity
contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
    DelegateERC20 public delegate;

    // ... access protected functions

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

The LGT token has a custom `transfer` function that delegates the transfer to another contract if `delegate` variable is set, the `delegate` being the `DoubleEntryPoint` contract.

In the DET token, we have a modifier and a `delegateTransfer()` function (we don't need anything related to Forta at this point):

```solidity
contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {

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

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

The modifier is making sure that only the `delegatedFrom` contract can call the `delegateTransfer` function. The `delegateTransfer` function is the one that is called by the `LegacyToken` contract.

Now, back to the `CryptoVault` contract. There is nothing really interesting except the `sweeppToken()` function.

```solidity
function sweepToken(IERC20 token) public {
    require(token != underlying, "Can't transfer underlying token");
    token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}
```

This function allows to transfer the whole balance of any token from the `CryptoVault` contract to the `sweptTokensRecipient`, as long as it is not the `underlying` token.

OK. But... what if we try to transfer the LGT token instead?

The `CryptoVault::sweepToken` will call the `LegacyToken::transfer` which will forward the call to the `DoubleEntryPoint::delegateTransfer`.

At this point, `msg.sender` is the `CryptoVault` contract and `value` is the `CryptoVault` LGT's balance (equal to the DET balance). So this will bypass the `onlyDelegateFrom` check. Only this is not transferring the LGT token anymore, but the `underlying` DET token since we are inside the DET contract!

Here is the chain of events illustrated:

1. `CryptoVault::sweepToken(LGT)`
    
2. `LegacyToken::transfer({ to: sweptTokenRecipient, value: LGT.balanceof(CryptoVault) })`
    
3. `DoubleEntryPoint::delegateTransfer({ to: sweptTokenRecipient, value: LGT.balanceof(CryptoVault), origSender: CryptoVault })`
    

### Implement the Forta bot

To mitigate this vulnerability, we need to implement a Forta AlertBot that will trigger an alert if `origSender == cryptoVault` in the `delegateTransfer` function, via the `fortaNotify` modifier.

In the `Forta` contract, there is a `setDetectionBot` function that we can use to implement our bot. And we have the following `IDetectionBot` interface:

```solidity
interface IDetectionBot {
    function handleTransaction(address user, bytes calldata msgData) external;
}
```

So we have to create the logic for the `handleTransaction()` function that will call the `IForta.raiseAlert` function if `origSender == cryptoVault`.

`DoubleEntryPoint::fortaNotify()` is passing `msg.data` to the `Forta::notify()` function.

* Initially, `msg.data` is the data for the `function delegateTransfer(address to, uint256 value, address origSender)` function.
    
* Then, in `Forta::notify()`, the data is updated to the data of the `handleTransaction(user, msgData)` function.
    

So that will look something like this:

> handleTransaction(user, msgData);
> 
> > Where `msgData == delegateTransfer(to, value, origSender);`

So to access the `origSender` variable, we need to understand how a calldata is structured.

| Position | Bytes/Length | Type | Value |
| --- | --- | --- | --- |
| 0x00 | 4 | bytes4 | selector of `handleTransaction()` |
| 0x04 | 32 | address | `user` address |
| 0x24 | 32 | uint256 | Offset of `msgData` |
| 0x44 | 32 | uint256 | Length of `msgData` |
| 0x64 | 4 | bytes4 | selector of `delegateTransfer()` |
| 0x68 | 32 | address | `to` address |
| 0x88 | 32 | uint256 | `value` parameter |
| 0xa8 | 32 | address | \=&gt; `origSender` address |

We can use a bit of assembly to access the `origSender` variable:

```solidity
assembly {
    origSender := calldataload(0xa8)
}
```

## Solution

Here is the full solution for the `AlertBot` contract:

```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IDetectionBot {
    function handleTransaction(address user, bytes calldata msgData) external;
}

interface IForta {
    function raiseAlert(address user) external;
}

contract AlertBot is IDetectionBot {
    address private immutable cryptoVault;

    constructor(address _cryptoVault) public {
        cryptoVault = _cryptoVault;
    }

    function handleTransaction(
        address user,
        bytes calldata msgData
    ) external override {
        address origSender;
        assembly {
            origSender := calldataload(0xa8)
        }

        if (origSender == cryptoVault) {
            IForta(msg.sender).raiseAlert(user);
        }
    }
}
```

Next, let's write the script that we will use to deploy our contract:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {AlertBot} from "../src/26_DoubleEntryPoint.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IDoubleEntryPoint {
    function cryptoVault() external view returns (address);
    function forta() external view returns (IForta);
}

interface IForta {
    function setDetectionBot(address detectionBotAddress) external;
}

contract PoC is Script {
    // Replace with your DoubleEntryPoint instance
    IDoubleEntryPoint doubleEntryPoint =
        IDoubleEntryPoint(0x1Ac3aD234aFEE64572c28242BF0197E1e8CaA717);

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployer);

        address cryptoVault = doubleEntryPoint.cryptoVault();
        AlertBot alertBot = new AlertBot(cryptoVault);
        doubleEntryPoint.forta().setDetectionBot(address(alertBot));

        vm.stopBroadcast();
    }
}
```

Then you can run the script with the following command:

```bash
forge script script/26_DoubleEntryPoint.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY
```

**🎉 Level completed 🎉**

## Takeaway

* Detection Bots can be used to monitor any activity on the blockchain and trigger alerts.
    

## Reference

* Forta bot: [https://docs.forta.network/en/latest/](https://docs.forta.network/en/latest/)
    
* CallData structure: [https://docs.soliditylang.org/en/latest/abi-spec.html#abi](https://docs.soliditylang.org/en/latest/abi-spec.html#abi)
    

---

You can find all the codes, challenges and their solutions on my GitHub: [https://github.com/Pedrojok01/Ethernaut-Solutions/](https://github.com/Pedrojok01/Ethernaut-Solutions/blob/main/solutions/01_Fallback.md)
