1

I'm trying to write unit tests for a Chainlink CCIPSender contract, but as far as I can tell it is reverting when calling getFee() to pay for the transaction.

In my test I call applyRampUpdates() from Chainlink CCIP's Router.sol. I have to specify a destination chain selector and an onRamp address. For the address, I have tried msg.sender, the receiver contract and the router itself.

When I run my test, it reverts during the getFee() call.

This line in Router.getFee() implies I have to implement my own onRamp contract with custom getFee() logic:

return IEVM2AnyOnRamp(onRamp).getFee(destinationChainSelector, message);

Is that correct or am I wrong?

Here is my test:

 modifier fundLinkToRevealerAndApprove() {
        vm.prank(msg.sender);
        link.transfer(REVEALER, STARTING_USER_BALANCE);
        vm.deal(REVEALER, STARTING_USER_BALANCE);
        vm.prank(REVEALER);
        link.approve(address(ccidSender), oraclePayment);
        _;
    }

    modifier authorizedSenders() {
        address[] memory authorizedAddresses = new address[](1);
        authorizedAddresses[0] = REVEALER;

        vm.prank(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f);
        operator.setAuthorizedSenders(authorizedAddresses);
        _;
    }

    modifier setDestinationSelectorAndReceiverAddress() {
        vm.startPrank(anvilAccount);
        ccidSender.setCcidDestinationSelector(ccidDestinationSelector);
        ccidSender.setCcidReceiver(ccidReceiver);
        vm.stopPrank();
        _;
    }

    modifier applyRampUpdates() {
        vm.startPrank(msg.sender);
        Router.OnRamp memory newOnRamp = Router.OnRamp({
            destChainSelector: 16015286601757825753, // Sepolia
            onRamp: ccidReceiver
        });
        Router.OnRamp[] memory newOnRampArray = new Router.OnRamp[](1);
        newOnRampArray[0] = newOnRamp;

        router.applyRampUpdates(newOnRampArray, new Router.OffRamp[](0), new Router.OffRamp[](0));
        vm.stopPrank();
        _;
    }

    function testFulfillAndSend()
        public
        fundLinkToRevealerAndApprove
        authorizedSenders
        setDestinationSelectorAndReceiverAddress
        applyRampUpdates
    {
        vm.startPrank(REVEALER);
        ccidSender.requestStatus(REVEALEE);
        // bytes32 requestId = ccidSender.getLatestSentRequestId();
        bytes32 constantRequestId = 0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe;
        uint8 kycUserStatus = 1;
        uint40 nonZeroKycTimestamp = 1658845449;
        bytes memory data = abi.encode(constantRequestId, kycUserStatus, nonZeroKycTimestamp);
        operator.fulfillOracleRequest2(
            constantRequestId,
            oraclePayment,
            address(ccidSender),
            bytes4(keccak256("fulfill(bytes32,uint8,uint40)")),
            block.timestamp + 5 minutes,
            data
        );
        vm.stopPrank();
    }

The test function I am testing is testFulfillAndSend().

Here are the functions in my Sender contract that it calls:

function fulfill(bytes32 _requestId, Status _status, uint40 _kycTimestamp)
        external
        override
        recordChainlinkFulfillment(_requestId)
    {
        if (_status == Status.KYCUser) {
            if (_kycTimestamp == 0) {
                revert CCIDSender__KycTimestampShouldNotBeZeroForKycUser();
            }
        } else {
            if (_kycTimestamp != 0) {
                revert CCIDSender__KycTimestampShouldBeZeroForNonKycUser();
            }
        }

        Request storage request = _requests[_requestId];
        request.kycTimestamp = _kycTimestamp;
        request.isFulfilled = true;
        request.isHumanAndUnique = _status != Status.NotFound;
        request.isKYCUser = _status == Status.KYCUser;
        latestFulfilledRequestId[request.revealee] = _requestId;

        emit Fulfilled(_requestId, request.revealer, request.revealee, _status, _kycTimestamp);
        sendKycStatusToCcidReceiver(_status);
    }

    function sendKycStatusToCcidReceiver(Status _status) internal {
        string memory statusString = statusToString(_status);
        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(ccidReceiver),
            data: abi.encode(statusString),
            tokenAmounts: new Client.EVMTokenAmount[](0),
            extraArgs: "",
            feeToken: link
        });

        uint64 destinationChainSelector = ccidDestinationSelector;

        uint256 fees = IRouterClient(router).getFee(destinationChainSelector, message);
        LinkTokenInterface(link).approve(address(router), fees);

        IRouterClient(router).ccipSend(destinationChainSelector, message);
    }

Here are the final parts of my test log:

├─ [39126] Operator::fulfillOracleRequest2(0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe, 100000000000000000 [1e17], CCIDSender: [0x5FbDB2315678afecb367f032d93F642f64180aa3], 0xccd8682400000000000000000000000000000000000000000000000000000000, 301, 0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000062dff909) 
    │   ├─ emit OracleResponse(requestId: 0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe)
    │   ├─ [33555] CCIDSender::fulfill(0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe, 1, 1658845449 [1.658e9]) 
    │   │   ├─ emit ChainlinkFulfilled(id: 0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe)
    │   │   ├─ emit Fulfilled(_requestId: 0x5e8ebfa50e778e69264bdc847efd6c474992d0ba91772b41eb52d11737a9eafe, _revealer: REVEALER: [0x513dcA22893fBB7eB2C737A3b3D046d03D8371AD], _revealee: REVEALEE: [0x50FA752360aD481DDbE824f39Cbc32FA9FD9458d], _status: 1, _kycTimestamp: 1658845449 [1.658e9])
    │   │   ├─ [6860] Router::getFee(16015286601757825753 [1.601e19], EVM2AnyMessage { receiver: 0x000000000000000000000000c32c3d415b8597604342351d7246f85e29ad6efb, data: 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000084b59435f55534552000000000000000000000000000000000000000000000000, tokenAmounts: [], feeToken: 0xDB8cFf278adCCF9E9b5da745B44E754fC4EE3C76, extraArgs: 0x }) [staticcall]
    │   │   │   ├─ [0] ccidReceiver::getFee(16015286601757825753 [1.601e19], EVM2AnyMessage { receiver: 0x000000000000000000000000c32c3d415b8597604342351d7246f85e29ad6efb, data: 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000084b59435f55534552000000000000000000000000000000000000000000000000, tokenAmounts: [], feeToken: 0xDB8cFf278adCCF9E9b5da745B44E754fC4EE3C76, extraArgs: 0x }) [staticcall]
    │   │   │   │   └─ ← ()
    │   │   │   └─ ← "EvmError: Revert"
    │   │   └─ ← "EvmError: Revert"
    │   └─ ← false
    ├─ [0] VM::stopPrank() 
    │   └─ ← ()
    └─ ← ()

Whatever address I specify as the onRamp in my test's applyRampUpdates modifier is the address that getFee() attempts to be called from after the Router, so in that test log you can see the ccidReceiver address is getting called.

I have to call applyRampUpdates() on the Router in my test because I need to specify a destinationChainSelector.

If anyone could please offer any advice as to what I should be doing, I would be very grateful. To summarize: I am trying to test a CCIP Sender contract that appears to be reverting when calling getFee() and I am unclear about what address should be the onRamp, and the reasons for all of this. Thank you.

1 Answer 1

1

The getFee() call comes from the EVM2EVMOnRamp.sol smart contract.

So you need to either provide its actual address on the source blockchain, assuming you are using the forking method for testing, or with any other setup you will need to deploy it (or its mocked version) during the test setup.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.