ERC-7412 - On-Demand Off-Chain Data Retrieval

Created 2023-07-26
Status Draft
Category ERC
Type Standards Track
Authors

Abstract

Contracts may require off-chain data during execution. A smart contract function could implement the standard proposed here by reverting with error OracleDataRequired(address oracleContract, bytes oracleQuery). Clients supporting this standard would recognize this error message during a simulation of the request, query the specified decentralized oracle network for signed data, and instead stage a transaction with a multicall that prepends the verification of the required off-chain data. The data would be written on-chain during verification to a smart contract for the subsequent call to read, avoiding the error.

Motivation

Ethereum's scaling roadmap involves a series of separate execution contexts for smart contract code (including layer two and layer three scaling solutions). This makes the ability to read data across multiple chains crucial to the construction of scalable applications. Also, for decentralized finance protocols that rely on price data, it is not reasonable to expect oracle networks will be able to continuously push fresh data to every layer two and layer three network for an arbitrary number of price feeds.

Cross-chain bridges are being developed where smart contract functions can write data to other chains. There is a need for a similar standard that enables reading data from other chains. This standard can be generalized for reading any off-chain data from a decentralized oracle network, including price feeds.

With standards for both writing and reading cross-chain data, protocol developers will be able to create abstractions for asynchronicity (a topic thoroughly explored in other software engineering contexts). This will enable the development of highly sophisticated protocols that do not suffer from scaling constraints.

ERC-3668 introduced the use of reverts for requiring off-chain data, but there are various challenges introduced by the specifics of that standard which are outlined in the Rationale section below. By leveraging multicalls rather than callback functions, the standard proposed here is able to overcome some of these constraints.

Specification

A contract implementing this standard MUST revert with the following error whenever off-chain data is required:

error OracleDataRequired(address oracleContract, bytes oracleQuery)

oracleQuery specifies the off-chain data that is being required. Valid data formats for this parameter are specific to the oracle ID specified by the oracle contract. This might include chain id, contract address, function signature, payload, and timestamp/"latest" for cross-chain reads. For price feeds, it could include a ticker symbol and timestamp/"latest".

oracleContract is the address of the contract which can verify the off-chain data and provide it to the contract to avoid the OracleDataRequired error. This contract MUST implement the following interface:

interface IERC7412 {
  function oracleId() view external returns (bytes32 oracleId);
  function fulfillOracleQuery(bytes signedOffchainData) payable external;
}

oracleId is a unique identifier that references the decentralized oracle network that generates the desired signed off-chain data. Oracle IDs would be analogous to Chain IDs in the Ethereum ecosystem. Clients are expected to resolve a gateway that corresponds to an Oracle ID, similar to how clients are expected to resolve an RPC endpoint based on a Chain ID.

It should be possible to derive the oracleQuery from the signedOffchainData, such that the oracle contract is able to provide the verified offchain data based on the oracleQuery.

The contract implementing the IERC7412 interface MUST revert with the following error message if it requires payment to fulfill the oracle data query:

error FeeRequired(uint amount)

amount specifies the amount of native gas tokens required to execute the fulfillOracleQuery function, denominated in wei. This error MUST be resolved if the caller provides sufficient msg.value such that the fee amount can be collected by the oracle contract. The contract MAY NOT return gas tokens if they are provided in excess of the amount. In practice, we would expect the fee amount to remain relatively stable, if not constant.

It is the responsibility of the client to decide how to construct the multicall, where necessary the fulfillOracleQuery functions are being called before the intended function call in an atomic transaction. Wallets that support account abstraction (per ERC-4337) should already have the ability to generate atomic multi-operations. For EOA support, protocols could implement ERC-2771. A standard multicall contract can only be used to construct multicalls including functions which do not reference msg.sender or msg.data.

To prevent data becoming too stale for a request between the simulation and a call's execution, ideally a contract could also emit the following event: event OracleDataUsed(address oracleContract, bytes oracleQuery, uint expirationTime) Here, expirationTime is the time after which the OracleDataRequired error would be thrown by the contract. (This would typically be a calculation involving a staleness tolerance and block.timestamp). Client applications that implement this standard would be able to recognize this event during simulation and estimate if an additional update will still be necessary, taking into account the speed of the chain. For example, the oracle query may request the latest quote available for a particular price feed and the expiration time may signal that the price cannot be older than three seconds prior to the current timestamp recognized by the blockchain. This has been omitted from the standard because there isn't a practical way to retrieve event data during transaction simulations on most JSON-RPC APIs at this time.

Note that URI could be used as the oracleId with a URI specified as the oracleQuery. This would allow this standard to be compliant with arbitrary on-chain URIs without requiring updates to a client library, similar to ERC-3668.

Rationale

This proposal is essentially an alternative to ERC-3668 with a few important distinctions:

With this standard, not only can oracle providers scalably support an unlimited number of networks but they can also be compatible with local/forked networks for protocol development.

Another major advantage of this standard is that oracles can charge fees in the form of native gas tokens during the on-chain verification of the data. This creates an economic incentive where fees can be collected from data consumers and provided to node operators in the decentralized oracle network.

Reference Implementation

The following pseudocode illustrates an oversimplified version of the client SDK. Ideally, this could be implemented in wallets, but it could also be built into the application layer. This function takes a desired transaction and converts it into a multicall with the required data verification transactions prepended such that the OracleDataRequired errors would be avoided:

function prepareTransaction(originalTx) {
  let multicallTx = [originalTx];
  while (true) {
    try {
      const simulationResult = simulateTx(multicallTx);
      return multicallTx;
    } catch (error) {
      if (error instanceof OracleDataRequired) {
        const signedRequiredData = fetchOffchainData(
          error.oracleContract,
          error.oracleQuery
        );
        const dataVerificationTx = generateDataVerificationTx(
          error.oracleContract,
          signedRequiredData
        );
        multicallTx.unshift(dataVerificationTx);
      }
    }
  }
}

An oracle provider could create a contract (that might also perform some pre-processing) that would automatically trigger a request for off-chain data as follows:

contract OracleContract is IERC7412 {
  address public constant VERIFIER_CONTRACT = 0x0000;
  uint public constant STALENESS_TOLERANCE = 86400; // One day
  mapping(bytes32 => bytes) public latestVerifiedData;

  function oracleId() external pure returns (bytes32){
    return bytes32(abi.encodePacked("MY_ORACLE_ID"));
  }

  function fulfillOracleQuery(bytes calldata signedOffchainData) payable external {
    bytes memory oracleQuery = _verify(signedOffchainData);
    latestVerifiedData[keccak256(oracleQuery)] = signedOffchainData;
  }

  function retrieveCrossChainData(uint chainId, address contractAddress, bytes payload) internal returns (bytes) {
    bytes memory oracleQuery = abi.encode(chainId, contractAddress, payload);
    (uint timestamp, bytes response) = abi.decode(latestVerifiedData[oracleQuery], (uint, bytes));

    if(timestamp < block.timestamp - STALENESS_TOLERANCE){
      revert OracleDataRequired(address(this), oracleQuery);
    }

    return response;
  }

  function _verify(bytes memory signedOffchainData) payable internal returns (bytes oracleQuery) {
    // Insert verification code here
    // This may revert with error FeeRequired(uint amount)
  }

}

Now a top-level protocol smart contract could implement a cross-chain function like so:

interface ICrosschainContract {
  function functionA(uint x) external returns (uint y);
  function functionB(uint x) external returns (uint y);
}

contract CrosschainAdder {
  IERC7412 oracleContract = 0x0000;

  function add(uint chainIdA, address contractAddressA, uint chainIdB, address contractAddressB) external returns (uint sum){
    sum = abi.decode(oracleContract.retrieveCrossChainData(chainIdA, contractAddressA, abi.encodeWithSelector(ICrosschainContract.functionA.selector,1)), (uint)) + abi.decode(oracleContract.retrieveCrossChainData(chainIdB, contractAddressB, abi.encodeWithSelector(ICrosschainContract.functionB.selector,2)),(uint));
  }
}

Note that the developer of the CrosschainAdder function does not need to be concerned with the implementation of this standard. The add function can simply call the function on the oracle contract as if it were retrieving on-chain data normally.

Cross-chain functions like this could also be leveraged to avoid O(n) (and greater) loops on-chain. For example, chainIdA and chainIdB could reference the same chain that the CrosschainAdder contract is deployed on with functionA and functionB as view functions with computationally intensive loops.

Security Considerations

One potential risk introduced by this standard is that its reliance on multicalls could obfuscate transaction data in wallet applications that do not have more sophisticated transaction decoding functionality. This is an existing challenge being addressed by wallet application developers, as multicalls are increasingly common in protocol development outside of this standard.

Note that it is the responsibility of the verifier contract to confirm the validity of the data provided from the oracle network. This standard does not create any new opportunities for invalid data to be provided to a smart contract.

Copyright

Copyright and related rights waived via CC0.