Double Entry Point

Exploit a legacy contract interaction pattern involving multiple contracts and a shared dependency (CryptoVault) to drain funds.

Vulnerable Code
Analyze the Solidity code below to find the vulnerability.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Assume Forta is an external monitoring contract/service interface interface IForta { function setDetectionBot(address detectionBotAddress) external; function notify(address user, bytes calldata msgData) external; function raiseAlert(address user) external; } // Assume CryptoVault holds the underlying tokens contract CryptoVault { address public underlying; // The actual ERC20 token (e.g., DET) constructor(address _underlying) { underlying = _underlying; } // Simplified: Assume deposit/withdraw logic exists function transfer(address to, uint amount) external { IERC20(underlying).transfer(to, amount); } function sweepToken(address token) external { // Allows transferring *any* token held by the vault require(token != underlying, "Cannot sweep underlying"); IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this))); } } // Legacy contract interacting with the vault contract LegacyToken is IERC20 { CryptoVault public vault; // Standard ERC20 functions omitted for brevity... // Interactions with vault happen internally constructor(address _vault) { vault = CryptoVault(_vault); } } // The main vulnerable contract contract DoubleEntryPoint is IERC20 { // Implements ERC20, likely wraps DET address public player; CryptoVault public vault; address public delegatedToken; // Address of LegacyToken IForta public forta; constructor(address _vault, address _legacyToken, address _forta, address _player) { vault = CryptoVault(_vault); delegatedToken = _legacyToken; forta = IForta(_forta); player = _player; } // Delegate transfer logic (simplified) function transfer(address recipient, uint256 amount) public override returns (bool) { // Delegate actual transfer to CryptoVault vault.transfer(recipient, amount); return true; } // Other standard ERC20 functions (balanceOf, etc.) assumed to exist, // likely querying the vault or internal state. // Function to allow sweeping tokens *other than underlying* function sweepVaultToken(address token) external { // This seems redundant if CryptoVault.sweepToken exists? Maybe restricted access? vault.sweepToken(token); } // Function potentially used by Forta bot function delegateTransfer(address recipient, uint256 value, address origSender) public { // This seems designed to be called by a trusted bot? // Requires check that msg.sender is the registered Forta bot. // Transfer underlying tokens. require(msg.sender == address(forta), "Not Forta"); // Simplified check vault.transfer(recipient, value); } } // --- BOT Contract --- // Deployed by player, registered with Forta contract DetectionBot { IForta forta; address vaultAddress; address doubleEntryPointAddress; constructor(address _forta, address _vault, address _det) { forta = IForta(_forta); vaultAddress = _vault; doubleEntryPointAddress = _det; // Address of DoubleEntryPoint contract } function handleTransaction(address user, bytes calldata msgData) external { // Logic to detect malicious activity // If delegateTransfer is called originating from DoubleEntryPoint contract, raise alert (address target, ) = abi.decode(msgData[4:], (address, uint256)); // Decode target of internal call bytes4 selector = bytes4(msgData[:4]); // If DET token (DoubleEntryPoint) calls delegateTransfer... suspicious! // The goal is to alert IF DoubleEntryPoint contract *originates* a call to its own 'delegateTransfer' // This check is likely flawed. How can we trigger this? if (selector == DoubleEntryPoint.delegateTransfer.selector && target == doubleEntryPointAddress) { forta.raiseAlert(user); // Alert the user (player) } } }
Submit Explanation
Explain the vulnerability and how to exploit it.
Hints (5)
Just a little peak
Hint 1
Hint 2
Hint 3
Hint 4
Hint 5
Explanation
Discomfort = Learning