💀Potential Vulnerabilities in EVM Implementations: Overlooked DelegateCall in Precompiled Contracts

Blockchain technology has rapidly evolved over the years, and with it, the desire for improved efficiency and functionality has grown. In pursuit of these goals, several projects have ventured to implement their versions of the Ethereum Virtual Machine (EVM) and introduced custom precompiled contracts to enhance performance. However, while these innovations are commendable, a recurring oversight has emerged, which may pose significant security risks.

Understanding the Basics

Before delving into the potential vulnerabilities, it's crucial to understand the fundamental concepts:

  1. EVM: Ethereum's runtime environment, ensuring consistent smart contract execution across all network nodes.

  2. Precompiled Contracts: These are contracts whose functionality is hard-coded into the blockchain nodes. They're designed to perform specific functions more efficiently than a standard smart contract written in Solidity could. Given their hard-coded nature, they're less flexible than regular contracts but can be much faster and less gas-intensive.

  3. DelegateCall: A unique functionality in Ethereum smart contracts. It allows a contract to execute the code of another contract while preserving its own state. In simple terms, Contract A can "borrow" the logic of Contract B without changing Contract A's data.

The Overlooked DelegateCall

The DelegateCall function has proven to be a double-edged sword. While it can be handy for contract upgrades and logic reuse, it can also introduce vulnerabilities if not appropriately managed. This is especially true for projects that introduce custom precompiled contracts without adequately considering the implications of DelegateCall.

Here's the crux of the issue: When new EVM implementations create custom precompiled contracts, they inherently trust that these contracts are safe since they're hardcoded. But if these precompiled contracts are made accessible via DelegateCall from other contracts, and the EVM implementation hasn't been designed with this in mind, unforeseen behaviors might emerge.

The Off-chain Dilemma

The security concerns don't stop at the blockchain's edge. Many blockchain projects incorporate off-chain systems to improve scalability and efficiency. These systems, designed to work seamlessly with on-chain components, often rely on the predictability and security of the on-chain environment.

If a new EVM implementation's precompiled contracts are not designed to handle DelegateCall properly, malicious actors might exploit this oversight, triggering unpredictable behaviors in the associated off-chain systems. Since off-chain systems don't benefit from the same consensus and immutability mechanisms as on-chain processes, vulnerabilities here can have far-reaching implications.

Extended Example: The Misleading DelegateCall

Let's delve into an illustrative scenario to capture the described vulnerability in the presence of msg.value and payable.

Imagine a blockchain ecosystem with two main precompiled contracts - DepositContract and EventLogContract.

// DepositContract.sol
pragma solidity ^0.8.0;

contract DepositContract {
    EventLogContract eventLog = new EventLogContract();

    function acceptEther() public payable {
        // Logic based on the deposited ether
        eventLog.emitDepositEvent(msg.value);
    }
}

// EventLogContract.sol
pragma solidity ^0.8.0;

contract EventLogContract {
    event DepositMade(uint256 amount);

    function emitDepositEvent(uint256 amount) public {
        emit DepositMade(amount);
    }
}

Typically, users would send ether to the DepositContract using the acceptEther function. Based on the msg.value, the contract then calls EventLogContract to emit an event specifying the deposited ether amount. Off-chain systems, listening to these events, would then credit users on Layer 2, given the ether has been locked on Layer 1.

However, let's consider the scenario where a malicious smart contract uses DelegateCall to exploit this setup:

// MaliciousContract.sol
pragma solidity ^0.8.0;

contract MaliciousContract {
    DepositContract dc;

    constructor(address _dc) {
        dc = DepositContract(_dc);
    }

    function exploit() public payable {
        bytes4 methodId = bytes4(keccak256("acceptEther()"));
        (bool success,) = address(dc).delegatecall{value: msg.value}(abi.encodeWithSelector(methodId));
        require(success);
    }
}

Understanding the exploit:

  1. The malicious actor deploys MaliciousContract providing the address of the DepositContract.

  2. The actor then invokes the exploit function of the MaliciousContract, sending along some ether.

  3. Inside the exploit function, a DelegateCall is made to DepositContract's acceptEther function. Given it's a DelegateCall, the msg.value remains the amount sent by the attacker, but the ether is not actually transferred to the DepositContract.

  4. The logic of acceptEther is borrowed, and it misleadingly calls the emitDepositEvent of EventLogContract emitting an event that msg.value ether has been deposited.

  5. Off-chain systems, which are programmed to credit users based on these events, might be tricked into crediting the malicious user on Layer 2 without any actual deposit on Layer 1.

Outcome: The vulnerability exposes the DelegateCall feature's potential misuse, especially when combined with msg.value. Custom precompiled contracts and the off-chain systems listening to their events must be designed with a deep understanding and caution against such manipulations.

Any questions on the material so far? Ask Omar Inuwa:

LinkedIn, Twitter

Last updated