测试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调用报错是两个细节错误导致的:
- 函数签名传错:
pendingRequests是mapping(bytes32 => address)类型,Solidity自动为它生成的getter函数签名是pendingRequests(bytes32),不是无参的pendingRequests(),找不到对应存储槽自然会报目标存储不存在的错误 - 键类型不匹配:你传入的
"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
相关产品推荐
相关产品推荐

