Skip to content

bug(forge): incorrect trace label for receive vs fallback on raw bytecode #12962

@anon-cBE4

Description

@anon-cBE4

Component

Forge

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

forge --version forge Version: 1.5.1-stable Commit SHA: b0a9dd9 Build Timestamp: 2025-12-22T11:39:01.425730780Z (1766403541) Build Profile: maxperf

What version of Foundryup are you on?

foundryup: 1.5.0

What command(s) is the bug in?

forge test --match-path test/RuntimeLib.t.sol -vvvv

Operating System

Linux

Describe the bug

Reproduce:

  1. Generate deployed bytecode.
  2. Deploy the raw bytecode.
  3. deployed.call{value: 0}("");. And receive is invoked.

Expect: trace is receive
Actual: trace is fallback

msg.data is empty, so the correct trace is receive.

$ forge test --match-path test/RuntimeLib.t.sol -vvvv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for test/RuntimeLib.t.sol:RuntimeLibTest
[PASS] testDeployRawBytecode() (gas: 186959)
Traces:
  [186959] RuntimeLibTest::testDeployRawBytecode()
    ├─ [147963] → new <unknown>@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
    │   └─ ← [Return] 739 bytes of code
    ├─ [3296] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::fallback()
    │   ├─ emit Log(: "receive", : RuntimeLibTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], : 0, : 0x)
    │   └─ ← [Stop]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 243.92µs (77.35µs CPU time)

0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::fallback() vs emit Log(: "receive"

// Runtime.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "./RuntimeLib.sol";

/*
contract Router {
    event Log(string func, address sender, uint value, bytes data);

    receive() external payable {
        emit Log("receive", msg.sender, msg.value, "");
    }

    fallback() external payable {
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }
}
*/

contract RuntimeLibTest is Test {
    function testDeployRawBytecode() public {
        // Raw runtime bytecode of the Router contract
        bytes memory code = hex"608060405236610044577ff7f75251dee7d7fc22deac3247729ebe7c86541f35930bf10c2a4207479a3b6c333460405161003a929190610172565b60405180910390a1005b7ff7f75251dee7d7fc22deac3247729ebe7c86541f35930bf10c2a4207479a3b6c333460003660405161007a949392919061025a565b60405180910390a1005b600082825260208201905092915050565b7f7265636569766500000000000000000000000000000000000000000000000000600082015250565b60006100cb600783610084565b91506100d682610095565b602082019050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061010c826100e1565b9050919050565b61011c81610101565b82525050565b6000819050919050565b61013581610122565b82525050565b600082825260208201905092915050565b50565b600061015c60008361013b565b91506101678261014c565b600082019050919050565b6000608082019050818103600083015261018b816100be565b905061019a6020830185610113565b6101a7604083018461012c565b81810360608301526101b88161014f565b90509392505050565b7f66616c6c6261636b000000000000000000000000000000000000000000000000600082015250565b60006101f7600883610084565b9150610202826101c1565b602082019050919050565b82818337600083830152505050565b6000601f19601f8301169050919050565b6000610239838561013b565b935061024683858461020d565b61024f8361021c565b840190509392505050565b60006080820190508181036000830152610273816101ea565b90506102826020830187610113565b61028f604083018661012c565b81810360608301526102a281848661022d565b90509594505050505056fea2646970667358221220749a95417d16869c0020868fb950c3602810c6148809abb4489b45f51181536964736f6c63430008130033";

        // Deploy using RuntimeLib
        address deployed = RuntimeLib.deploy(code);
        
        // Assert deployment was successful
        assertTrue(deployed != address(0), "Deployment failed, address is zero");

        // Send 0 ETH and empty data
        // This should trigger the receive() function (0x72656365697665... in hex)
        (bool success, ) = deployed.call{value: 0}("");
        assertTrue(success, "Call to deployed contract failed");
    }
}

Following is deploy helper.

// RuntimeLib.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.19;
import "./BytesLib.sol";

library RuntimeLib {
    function deploy(bytes memory code) internal returns (address) {
        // Prepends a minimal constructor to save some gas
        bytes memory initcode = getInitCode(code);
        address addr;
        assembly {
            addr := create(0, add(initcode, 0x20), mload(initcode))
        }

        require(addr != address(0), "failed to deploy contract");

        return addr;
    }

    function getInitCode(
        bytes memory code
    ) private pure returns (bytes memory) {
        // PUSH size, offset, destoffset; CODECOPY; PUSH size, offset; RETURN;
        bytes memory init = hex"610000600E6000396100006000F3";
        //                        ----            ----
        // Set the size of the runtime bytecode
        uint256 len = code.length;
        assembly {
            let lowerByte := and(0xff, len)
            let upperByte := shr(8, len)

            mstore8(add(init, 0x21), upperByte)
            mstore8(add(init, 0x22), lowerByte)

            mstore8(add(init, 0x29), upperByte)
            mstore8(add(init, 0x2A), lowerByte)
        }

        return BytesLib.concat(init, code);
    }
}

// BytesLib.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.8.19;

// Copied and unchanged from GNSPS/solidity-bytes-utils
// https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol

library BytesLib {
    function concat(
        bytes memory _preBytes,
        bytes memory _postBytes
    ) internal pure returns (bytes memory) {
        bytes memory tempBytes;

        assembly {
            // Get a location of some free memory and store it in tempBytes as
            // Solidity does for memory variables.
            tempBytes := mload(0x40)

            // Store the length of the first bytes array at the beginning of
            // the memory for tempBytes.
            let length := mload(_preBytes)
            mstore(tempBytes, length)

            // Maintain a memory counter for the current write location in the
            // temp bytes array by adding the 32 bytes for the array length to
            // the starting location.
            let mc := add(tempBytes, 0x20)
            // Stop copying when the memory counter reaches the length of the
            // first bytes array.
            let end := add(mc, length)

            for {
                // Initialize a copy counter to the start of the _preBytes data,
                // 32 bytes into its memory.
                let cc := add(_preBytes, 0x20)
            } lt(mc, end) {
                // Increase both counters by 32 bytes each iteration.
                mc := add(mc, 0x20)
                cc := add(cc, 0x20)
            } {
                // Write the _preBytes data into the tempBytes memory 32 bytes
                // at a time.
                mstore(mc, mload(cc))
            }

            // Add the length of _postBytes to the current length of tempBytes
            // and store it as the new length in the first 32 bytes of the
            // tempBytes memory.
            length := mload(_postBytes)
            mstore(tempBytes, add(length, mload(tempBytes)))

            // Move the memory counter back from a multiple of 0x20 to the
            // actual end of the _preBytes data.
            mc := end
            // Stop copying when the memory counter reaches the new combined
            // length of the arrays.
            end := add(mc, length)

            for {
                let cc := add(_postBytes, 0x20)
            } lt(mc, end) {
                mc := add(mc, 0x20)
                cc := add(cc, 0x20)
            } {
                mstore(mc, mload(cc))
            }

            // Update the free-memory pointer by padding our last write location
            // to 32 bytes: add 31 bytes to the end of tempBytes to move to the
            // next 32 byte block, then round down to the nearest multiple of
            // 32. If the sum of the length of the two arrays is zero then add
            // one before rounding down to leave a blank 32 bytes (the length block with 0).
            mstore(
                0x40,
                and(
                    add(add(end, iszero(add(length, mload(_preBytes)))), 31),
                    not(31) // Round down to the nearest 32 bytes.
                )
            )
        }

        return tempBytes;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions