Skip to content

bug(cheatcodes): unexpected behavior when using mockCall for payable functions #10812

Open
@srdtrk

Description

@srdtrk

Component

Forge

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

  • Foundry
  • Foundryup

What version of Foundry are you on?

forge Version: 1.2.3-nightly
Commit SHA: bfc53de
Build Timestamp: 2025-06-19T06:03:45.745585000Z (1750313025)
Build Profile: maxperf

What version of Foundryup are you on?

Installed via nix

What command(s) is the bug in?

forge test

Operating System

macOS (Apple Silicon)

Describe the bug

If the mock call simulates a payable function, then when the address is called, the call succeeds but the balance is not transferred.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import { Test } from "forge-std/Test.sol";

contract MockCallBugTest is Test {
    function setUp() public { }

    function testMockCallBug() public {
        vm.deal(address(this), 1 ether);

        address target = makeAddr("target");
        uint256 value = 0.1 ether;
        bytes memory data = "someData";
        bytes memory resp = "mockedResponse";

        assertEq(target.balance, 0, "Initial balance should be zero");

        // Mock a call to the target address
        vm.mockCall(target, value, data, resp);

        // Make the call
        (bool success, bytes memory returnData) = target.call{value: value}(data);
        assertTrue(success, "Call should succeed");
        assertEq(returnData, resp, "Return data should match");

        // Check the target's balance after the call
        assertEq(target.balance, 0, "Target balance should NOT be zero after mock call");
        // BUG: This is a bug since the value is not transferred to the target but the call is not reverted.
        // should pass: assertEq(target.balance, value, "Target balance should NOT be zero after mock call");
    }

    function testRealCall() public {
        vm.deal(address(this), 1 ether);

        address target = address(new RealCallContract());
        uint256 value = 0.1 ether;
        bytes memory data = abi.encodeCall(RealCallContract.payableFunction, ());
        bytes memory resp = abi.encode("mockedResponse");

        assertEq(target.balance, 0, "Initial balance should be zero");

        // Make the real call
        (bool success, bytes memory returnData) = target.call{value: value}(data);
        assertTrue(success, "Call should succeed");
        assertEq(returnData, resp, "Return data should match");

        // Check the target's balance after the call
        assertEq(target.balance, value, "Target balance should be equal to the value sent");
    }
}

contract RealCallContract {
    function payableFunction() external payable returns (bytes memory) {
        return "mockedResponse";
    }
}

I don't know why this occurs. I have verified this behavior with slight variations:

  • Making sure target has some code length. (vm.etch(target, "mock"))
  • Casting target into a payable address

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-bugType: bugT-to-discussType: requires discussion

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions