Skip to content

Inconsistent behaviour of vm.mockFunction #13110

@avniculae

Description

@avniculae

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.5.1-stable Commit SHA: b0a9dd9 Build Timestamp: 2025-12-22T11:41:09.812070000Z (1766403669) Build Profile: maxperf

What version of Foundryup are you on?

foundryup: 1.0.1

What command(s) is the bug in?

forge test

Operating System

macOS (Apple Silicon)

Describe the bug

I am observing inconsistent behaviour of vm.mockFunction. Given there is no cheatcode to clear mocked functions, I am using vm.mockFunction(addr1, addr1, ...) to effectively clear mocked functions. However, this results in an infinite loop when used against proxies.

In the POC below, you'll see that test_mockFunction_proxy results in an infinite loop, while test_mockFunction_impl works fine. The only solution I found to "clear" mocked functions on proxies is to redirect to implementation (see test_mockFunction_proxy_impl).

test_mockFunction_impl succeeding and test_mockFunction_proxy failing does not make sense to me, since both tests do the same thing: use address' bytecode to cancel previous mocks.

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

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

contract Impl {
  function foo() public pure returns (string memory) {
    return 'foo';
  }
}

contract Proxy {
  address public impl;
  constructor(address impl_) {
    impl = impl_;
  }

  fallback() external {
    _delegate(impl);
  }

  // code from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/239795bea728c8dca4deb6c66856dd58a6991112/contracts/proxy/Proxy.sol#L22-L45
  function _delegate(address implementation) internal virtual {
    assembly {
      // Copy msg.data. We take full control of memory in this inline assembly
      // block because it will not return to Solidity code. We overwrite the
      // Solidity scratch pad at memory position 0.
      calldatacopy(0x00, 0x00, calldatasize())

      // Call the implementation.
      // out and outsize are 0 because we don't know the size yet.
      let result := delegatecall(gas(), implementation, 0x00, calldatasize(), 0x00, 0x00)

      // Copy the returned data.
      returndatacopy(0x00, 0x00, returndatasize())

      switch result
      // delegatecall returns 0 on error.
      case 0 {
        revert(0x00, returndatasize())
      }
      default {
        return(0x00, returndatasize())
      }
    }
  }
}

contract MockImpl {
  function foo() public pure returns (string memory) {
    return 'bar';
  }
}

contract MockFunctionTest is Test {
  Impl public impl;
  Impl public proxy;
  MockImpl public mockImpl;

  function setUp() public {
    impl = new Impl();
    proxy = Impl(address(new Proxy(address(impl))));
    mockImpl = new MockImpl();
  }

  function test_mockFunction_impl() public {
    vm.mockFunction(address(impl), address(mockImpl), abi.encodeWithSelector(Impl.foo.selector));
    assertEq(impl.foo(), 'bar');
    vm.mockFunction(address(impl), address(impl), abi.encodeWithSelector(Impl.foo.selector));
    assertEq(impl.foo(), 'foo');
  }

  function test_mockFunction_proxy() public {
    vm.mockFunction(address(proxy), address(mockImpl), abi.encodeWithSelector(Impl.foo.selector));
    assertEq(proxy.foo(), 'bar');
    vm.mockFunction(address(proxy), address(proxy), abi.encodeWithSelector(Impl.foo.selector));
    assertEq(proxy.foo(), 'foo');
  }

  function test_mockFunction_proxy_impl() public {
    vm.mockFunction(address(proxy), address(mockImpl), abi.encodeWithSelector(Impl.foo.selector));
    assertEq(proxy.foo(), 'bar');
    vm.mockFunction(address(proxy), address(impl), abi.encodeWithSelector(Impl.foo.selector));
    assertEq(proxy.foo(), 'foo');
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-bugType: bugT-needs-triageType: this issue needs to be labelled

    Type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions