You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

测试ChainlinkClient回调 如何绕过recordChainlinkFulfillment校验

问题背景

我编写了一个Chainlink客户端合约,会向预言机发送DirectRequest。预言机执行完成后,会通过ChainlinkRequest中传入的标准回调选择器返回结果,整套流程运行正常,但我需要编写测试用例验证回调逻辑的正确性。

客户端合约代码如下:

contract PriceFeed is Ownable, ChainlinkClient {
  function updatePrice() onlyOwner returns (bytes32 requestId) {
    // 发起Chainlink请求,通过this.requestCallback.selector指定回调函数
  }

  function requestCallback(bytes32 _requestId, uint256 _newPrice) public
        recordChainlinkFulfillment(_requestId) {
    price = _newPrice;
  }
}

测试时直接调用requestCallback(...)会触发recordChainlinkFulfillment(...)修饰符的校验逻辑:ChainlinkClient会判定传入的requestId不存在于私有pendingRequests映射中,直接抛出异常。

ChainlinkClient的简化实现如下:

contract ChainlinkClient {
  mapping(bytes32 => address) private pendingRequests;

  modifier recordChainlinkFulfillment(bytes32 _requestId) {
    require(msg.sender == pendingRequests[_requestId], "Source must be the oracle of the request");
    delete pendingRequests[_requestId];
    emit ChainlinkFulfilled(_requestId);
    _;
  }
}

初始测试代码如下,运行直接报错Source must be the oracle of the request:

contract PriceFeedTest is Test {
  function testInitialCallback() public {
      priceFeed.requestCallback("abc123", 1000000); // 执行失败
      assertEq(1000000, priceFeed.price(), "Expecting price to be 1000000");
  }
}

我尝试用Foundry的stdstorage作弊码预填充私有映射,但运行时报错No storage use detected for target,对应代码如下:

contract PriceFeedTest is Test {
  function testInitialCallback2() public {
      stdstore
          .target(address(priceFeed))
          .sig("pendingRequests()")
          .with_key("abc123")
          .checked_write(address(this));

      priceFeed.requestCallback("abc123", 1000000);
      assertEq(1000000, priceFeed.price(), "Expecting price to be 1000000");
  }
}

需要找到可行方案通过修饰符校验,完成回调逻辑测试。


解决方案

你的stdstorage调用报错是两个细节错误导致的:

  1. 函数签名传错:pendingRequests是mapping(bytes32 => address)类型,Solidity自动为它生成的getter函数签名是pendingRequests(bytes32),不是无参的pendingRequests(),找不到对应存储槽自然会报目标存储不存在的错误
  2. 键类型不匹配:你传入的"abc123"是string类型,而映射的键要求是bytes32,必须做显式类型转换,否则写入的存储位置完全不对

修正后的测试代码可以直接运行:

contract PriceFeedTest is Test {
  function testCallbackLogic() public {
    bytes32 testReqId = bytes32("abc123");
    stdstore
      .target(address(priceFeed))
      .sig("pendingRequests(bytes32)")
      .with_key(testReqId)
      .checked_write(address(this)); // 将当前测试地址设为该requestId对应的合法预言机地址

    priceFeed.requestCallback(testReqId, 1000000);
    assertEq(priceFeed.price(), 1000000, "price should update after callback");
  }
}

如果不想直接写入合约存储,还有两个更贴近真实线上流程的测试方案可选:

  • 先调用updatePrice()发起真实请求,拿到返回的requestId,再通过vm.prank作弊码把调用者伪装成对应预言机地址,触发回调,完全复现线上运行逻辑
  • 部署Chainlink预言机的Mock合约,所有请求都走Mock预言机触发fulfill流程,不需要手动修改合约状态

不需要修改业务合约的可见性或者加测试后门,Foundry的作弊码本身就是设计来支持测试环境下直接读写链上状态的,私有状态在测试环境下完全可以通过这种方式修改,不会影响主网部署的合约逻辑。


内容的提问来源于stack exchange,提问作者Ijonas Kisselbach

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 05:24:23