Skip to content
On this page

Securing the DoubleEntryPoint: A Deep Dive into Detection Bots and DeFi Exploit Prevention

Introduction

In the rapidly evolving world of decentralized finance (DeFi), security vulnerabilities can lead to catastrophic financial losses. The DoubleEntryPoint challenge from OpenZeppelin's Ethernaut platform presents a sophisticated scenario where a seemingly secure CryptoVault contains a subtle but dangerous vulnerability. This technical article explores the intricacies of this challenge, examining the architectural flaws, implementing a detection bot solution, and providing comprehensive insights into secure smart contract design patterns.

Understanding the Challenge Architecture

The Core Components

The DoubleEntryPoint ecosystem consists of several interconnected smart contracts, each serving a specific purpose:

  1. CryptoVault: A contract designed to manage tokens with a special sweepToken function for retrieving stuck tokens
  2. LegacyToken (LGT): An older token implementation that can delegate transfers to a newer contract
  3. DoubleEntryPoint (DET): The main token contract implementing the ERC20 standard with additional security features
  4. Forta: A monitoring network that allows users to register detection bots for threat identification

The Vulnerability Context

The CryptoVault holds 100 units of both LegacyToken (LGT) and DoubleEntryPoint (DET). The sweepToken function is designed to prevent sweeping of the underlying token (DET), which serves as a core logic component. However, this protection mechanism contains a critical flaw that could allow an attacker to drain the vault.

Analyzing the Vulnerability

The SweepToken Function

Let's examine the vulnerable sweepToken function in detail:

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

At first glance, this appears secure—it prevents direct transfer of the underlying DET token. However, the vulnerability lies in the interaction between LegacyToken and DoubleEntryPoint.

The Delegation Mechanism

The LegacyToken contract contains a delegation feature:

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

When a delegate contract is set, LegacyToken transfers are redirected through the delegateTransfer function of the DoubleEntryPoint contract.

The Exploit Vector

The critical insight is that when sweepToken is called with the LegacyToken address, it triggers a transfer that gets delegated to DoubleEntryPoint's delegateTransfer. This function then performs a transfer from origSender (the CryptoVault) to the recipient, effectively bypassing the require(token != underlying) check.

The attack flow:

  1. Attacker calls sweepToken(LegacyTokenAddress)
  2. CryptoVault checks LegacyToken != DET (passes)
  3. LegacyToken's transfer function is called
  4. Since delegate is set to DoubleEntryPoint, it calls delegateTransfer
  5. DoubleEntryPoint transfers DET tokens from CryptoVault to attacker
  6. The underlying token protection is circumvented

The Forta Detection System

Understanding Forta's Role

Forta provides a decentralized monitoring framework where users can register custom detection bots. These bots analyze transaction data and can raise alerts to prevent malicious activities.

The Forta contract implementation:

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

The Detection Bot Interface

Detection bots must implement the IDetectionBot interface:

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

When Forta's notify function is called, it invokes the registered bot's handleTransaction method with the user address and transaction data.

Implementing the Detection Bot

Analyzing the DoubleEntryPoint Security Modifier

The DoubleEntryPoint contract includes a crucial security modifier:

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

This modifier is applied to the delegateTransfer function, ensuring that any suspicious delegation transfer is monitored and can be prevented.

Crafting the Detection Logic

The detection bot needs to identify when delegateTransfer is being called with the CryptoVault as the origSender. Here's the complete implementation:

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

import "./DoubleEntryPoint.sol";

contract Hack is IDetectionBot {
    IForta private immutable target;
    address private immutable cryptoVault;

    constructor(address _target, address _cryptoVault) {
        target = IForta(_target);
        cryptoVault = _cryptoVault;
    }

    function handleTransaction(address user, bytes calldata msgData) external {
        require(msg.sender == address(target), "Unauthorized");
        
        // Decode the function selector and parameters
        bytes4 selector = bytes4(msgData[:4]);
        
        // Check if this is a delegateTransfer call
        if (selector == DoubleEntryPoint.delegateTransfer.selector) {
            // Decode the parameters: (address to, uint256 value, address origSender)
            (address to, uint256 value, address origSender) = abi.decode(
                msgData[4:], 
                (address, uint256, address)
            );
            
            // Check if the origSender is the CryptoVault
            if (origSender == cryptoVault) {
                // This is a suspicious transfer from the vault
                target.raiseAlert(user);
            }
        }
    }
}

Key Implementation Details

  1. Function Selector Analysis: The bot extracts the function selector from msgData to identify delegateTransfer calls
  2. Parameter Decoding: Using abi.decode to extract the function parameters
  3. Origin Validation: Checking if origSender matches the CryptoVault address
  4. Alert Mechanism: Calling raiseAlert when suspicious activity is detected

Testing the Solution

Comprehensive Test Implementation

Here's a complete test suite to verify the detection bot functionality:

typescript
import { ethers } from "hardhat";
import { expect } from "chai";
import { LegacyToken, Forta, CryptoVault, DoubleEntryPoint, Hack } from "../typechain-types";

describe("DoubleEntryPoint", function () {
  let legacyToken: LegacyToken;
  let forta: Forta;
  let cryptoVault: CryptoVault;
  let doubleEntryPoint: DoubleEntryPoint;
  let hack: Hack;
  let deployer: any;
  let attacker: any;

  beforeEach(async function () {
    [deployer, attacker] = await ethers.getSigners();

    // Deploy LegacyToken
    const LegacyTokenFactory = await ethers.getContractFactory("LegacyToken");
    legacyToken = await LegacyTokenFactory.deploy();
    await legacyToken.deployed();

    // Deploy Forta
    const FortaFactory = await ethers.getContractFactory("Forta");
    forta = await FortaFactory.deploy();
    await forta.deployed();

    // Deploy CryptoVault
    const CryptoVaultFactory = await ethers.getContractFactory("CryptoVault");
    cryptoVault = await CryptoVaultFactory.deploy(attacker.address);
    await cryptoVault.deployed();

    // Deploy DoubleEntryPoint
    const DoubleEntryPointFactory = await ethers.getContractFactory("DoubleEntryPoint");
    doubleEntryPoint = await DoubleEntryPointFactory.deploy(
      legacyToken.address,
      cryptoVault.address,
      forta.address,
      deployer.address
    );
    await doubleEntryPoint.deployed();

    // Set up token relationships
    await cryptoVault.setUnderlying(doubleEntryPoint.address);
    await legacyToken.delegateToNewContract(doubleEntryPoint.address);

    // Mint tokens to CryptoVault
    await legacyToken.mint(cryptoVault.address, ethers.utils.parseEther("100"));
  });

  describe("Vulnerability Demonstration", function () {
    it("should demonstrate the original vulnerability", async function () {
      // Initial balances
      const initialVaultBalance = await doubleEntryPoint.balanceOf(cryptoVault.address);
      console.log("Initial DET balance in vault:", ethers.utils.formatEther(initialVaultBalance));

      // Attempt to sweep LegacyToken (this triggers the vulnerability)
      await cryptoVault.connect(attacker).sweepToken(legacyToken.address);

      // Check if DET tokens were drained
      const finalVaultBalance = await doubleEntryPoint.balanceOf(cryptoVault.address);
      console.log("Final DET balance in vault:", ethers.utils.formatEther(finalVaultBalance));

      expect(finalVaultBalance).to.equal(0);
    });
  });

  describe("Detection Bot Protection", function () {
    it("should prevent token draining with detection bot", async function () {
      // Deploy and register detection bot
      const HackFactory = await ethers.getContractFactory("Hack");
      hack = await HackFactory.deploy(forta.address, cryptoVault.address);
      await hack.deployed();

      await forta.setDetectionBot(hack.address);

      // Verify bot registration
      const registeredBot = await forta.usersDetectionBots(deployer.address);
      expect(registeredBot).to.equal(hack.address);

      // Attempt to sweep tokens (should be blocked)
      await expect(
        cryptoVault.connect(attacker).sweepToken(legacyToken.address)
      ).to.be.revertedWith("Alert has been triggered, reverting");

      // Verify tokens are still safe
      const vaultBalance = await doubleEntryPoint.balanceOf(cryptoVault.address);
      expect(vaultBalance).to.be.greaterThan(0);
    });

    it("should allow legitimate transfers", async function () {
      // Deploy and register detection bot
      const HackFactory = await ethers.getContractFactory("Hack");
      hack = await HackFactory.deploy(forta.address, cryptoVault.address);
      await hack.deployed();

      await forta.setDetectionBot(hack.address);

      // Perform a legitimate transfer (not from vault)
      const transferAmount = ethers.utils.parseEther("10");
      await doubleEntryPoint.transfer(attacker.address, transferAmount);

      // Verify transfer succeeded
      const attackerBalance = await doubleEntryPoint.balanceOf(attacker.address);
      expect(attackerBalance).to.equal(transferAmount);
    });
  });
});

Security Best Practices and Lessons Learned

1. Comprehensive Input Validation

The original vulnerability stemmed from insufficient validation of indirect token transfers. When designing secure contracts:

solidity
// Instead of simple address comparison
require(token != underlying, "Can't transfer underlying token");

// Consider additional checks for delegated tokens
function sweepToken(IERC20 token) public {
    require(token != underlying, "Can't transfer underlying token");
    
    // Check if token delegates to underlying
    if (isDelegatedToken(token, underlying)) {
        revert("Cannot sweep tokens that delegate to underlying");
    }
    
    token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
}

2. Defense in Depth with Monitoring Systems

The Forta integration demonstrates the power of external monitoring systems:

  • Real-time detection: Bots can analyze transactions as they occur
  • Community-driven security: Multiple detection strategies can be deployed
  • Graceful failure: Transactions can be reverted before damage occurs

3. Secure Delegation Patterns

When implementing delegation patterns:

solidity
// Add explicit origin validation
function delegateTransfer(address to, uint256 value, address origSender)
    public
    override
    onlyDelegateFrom
    fortaNotify
    returns (bool)
{
    // Additional security check
    require(!isProtectedAddress(origSender), "Protected address cannot be origin");
    
    _transfer(origSender, to, value);
    return true;
}

4. Comprehensive Testing Strategies

Develop thorough test suites that cover:

  • Normal operation scenarios
  • Edge cases and boundary conditions
  • Attack vectors and exploit attempts
  • Integration with security systems

Advanced Detection Bot Techniques

Pattern Recognition

Sophisticated detection bots can implement pattern recognition:

solidity
contract AdvancedDetectionBot is IDetectionBot {
    struct TransactionPattern {
        address target;
        bytes4 selector;
        uint256 minValue;
        uint256 maxValue;
    }
    
    TransactionPattern[] private suspiciousPatterns;
    
    function addSuspiciousPattern(
        address _target,
        bytes4 _selector,
        uint256 _minValue,
        uint256 _maxValue
    ) external onlyOwner {
        suspiciousPatterns.push(TransactionPattern({
            target: _target,
            selector: _selector,
            minValue: _minValue,
            maxValue: _maxValue
        }));
    }
    
    function handleTransaction(address user, bytes calldata msgData) external override {
        // Analyze transaction against known patterns
        for (uint i = 0; i < suspiciousPatterns.length; i++) {
            TransactionPattern memory pattern = suspiciousPatterns[i];
            
            if (matchesPattern(msgData, pattern)) {
                raiseAlert(user, i);
                return;
            }
        }
    }
    
    function matchesPattern(bytes calldata data, TransactionPattern memory pattern) 
        internal pure returns (bool) 
    {
        // Implementation of pattern matching logic
        // ...
    }
}

Machine Learning Integration

Future detection systems could integrate machine learning models for anomaly detection, though this requires careful consideration of gas costs and on-chain limitations.

Conclusion

The DoubleEntryPoint challenge provides valuable insights into DeFi security architecture. Key takeaways include:

  1. Indirect attack vectors can bypass seemingly secure checks
  2. Delegation patterns require careful security consideration
  3. External monitoring systems like Forta provide crucial defense layers
  4. Comprehensive testing must include integration scenarios
  5. Defense in depth remains essential in smart contract security

By implementing robust detection bots and following security best practices, developers can create more resilient DeFi systems that protect user assets while maintaining functionality. The evolving landscape of blockchain security demands continuous learning and adaptation to emerging threats and protection mechanisms.

Further Reading and Resources

  1. OpenZeppelin Security Guidelines
  2. Forta Network Documentation
  3. Smart Contract Security Best Practices
  4. DeFi Attack Vectors and Mitigations
  5. Formal Verification in Smart Contracts

This comprehensive analysis demonstrates that while smart contract vulnerabilities can be subtle and complex, with proper architectural patterns, monitoring systems, and security practices, developers can build robust systems that protect user assets in the decentralized ecosystem.

Built with AiAda