CCIP Read update and call to action

I’ve spent the past week or so heads-down on CCIP Read, ENS and Chainlink’s solution to resolving ENS names from offchain data (including L2s), and wanted to give everyone a development update - and a call to action, as it’s now ready for devs to start playing with it.

First off, I’ve made substantial changes to the spec. It’s now formulated around offchain callbacks, which removes the need for a ‘prefix’ of the return data that the client validates, simplifies the interface for both contract and gateway server, and should make it easier to understand, as it’s now utilising a fairly standard callback mechanism. I think this also makes clear that there’s a wide variety of potential uses for this standard beyond ENS, too - it’s basically a “pull” protocol, where standard oracles are “push”.

I think it’s realistic to move the EIP to last call soon, and I’d like to get feedback from developers on any changes we should make before then.

Second, I have an open PR that substantially rewrites the CCIP-read Javascript tooling. It makes the following changes:

  • Implements the changes from the spec mentioned above.
  • Restructures the repository as a monorepo.
  • More READMEs and documentation.
  • Adds a package for an Ethers provider that allows you to use CCIP read in your webapp by changing a single line of code.
  • Rewrites the ‘trusted token’ example to use the new server framework and the Ethers provider.

The Ethers provider makes using CCIP read particularly simple. If you currently set up Ethers like this:

const ethers = require('ethers');

const provider = ethers.getDefaultProvider('mainnet');

Adding CCIP read support is as simple as doing this:

const ethers = require('ethers');
const ccipread = require('@smartcontractkit/ethers-ccip-read-provider');

const provider = new ccipread.CCIPReadProvider(Ethers.getDefaultProvider('mainnet'));

If you’re interested in exploring this functionality, please take a look - and come back with any feedback or criticisms you have on how easy and practical it is to use. I’ll aim to update this post with a more detailed “howto create a CCIP-read app and contract” post in the near future.

15 Likes

Great work on the spec!

Not sure if this is the best place to make a comment on the spec. Let me know if I should post it there instead : Durin: Secure offchain data retrieval - EIPs - Fellowship of Ethereum Magicians

I just got one little feedback that you probably though about :

Would be great if the standard supported GET request too so you could have ipfs:// url as url returned by the smart contract

This could look like the following:
ipfs://QmXnnyufdzAWL5CqZ2RnSNgPbvCc1ALT73s6epPrRnZ1Xy/<to>/<data>
(no need of request id)

We would also need to define the format for to and data, to could be the lower-case representation of the address and data could be the lower case hexadcimal representation prefixed with 0x

This would work well with merkle tree for example where merkle proof could be served that way

From the spec it does not seem to forbid such use, but I think having it part of the spec (like the POST json rpc request are) from the get-go would be beneficial. So tool can support it right away too

4 Likes

This is a really good point. In fact, it would make a lot of sense to do all lookups via GET, as that would simplify the protocol. I need to check with ChainLink if they’ll be able to host gateways if we use GET, though.

4 Likes

One example of a valid implementation of balanceOf would thus be:

function balanceOf(address addr) public view returns(uint balance) {
    revert OffchainLookup(
        address(this),
        [url],
        abi.encodeWithSelector(Gateway.getSignedBalance.selector, addr),
        ContractName.balanceOfWithProof.selector,
        abi.encode(addr)
    );
}

Note that in this example the contract is returning addr in both callData and extraData , because it is required both by the gateway (in order to look up the data) and the callback function (in order to verify it). The contract cannot simply pass it to the gateway and rely on it being returned in the response, as this would give the gateway an opportunity to respond with an answer to a different query than the one that was initially issued.

Doesn’t this open the door for poor/naive implementations where devs will fail to read the full spec and just rely on the correct response being returned? I notice this is pointed out below in the Security Considerations as well, but I’m worried about a hasty implementer missing this. What if the original callData was also passed to the callback as well?
(bytes originalCallData, bytes response, bytes extraData)

That way the callback is always guaranteed to receive the original data. And then extraData can still be used for anything that your callback needs but that you don’t want to send to the gateway (or other arbitrary contextual data).

Or, do you think that would be inappropriate/superfluous for most cases? It seems like it would be helpful for the current balanceOfWithProof example at least. But it doesn’t eliminate the problem: If verification is needed, implementers would still need to actually do that verification against originalCallData.

Admittedly this also means that you would probably be passing additional information to the callback function that it wouldn’t ever need, like Gateway.getSignedBalance.selector in this case.

So yeah, now that I’ve typed this out I can see the pros and cons, either way the dev is going to need to be aware of the security best practices here, and passing the original calldata might just be more overhead. Curious to hear your thoughts though!

Contract authors will need to be careful about how they treat data returned from the gateway, definitely - both making sure it validates, and making sure it answers the question originally posed. I don’t think we need to add another parameter for the original calldata, though, as implementations can include this themselves via the extra data if desired.