Onchain Write

This guide explains how to write data from your CRE workflow to a smart contract on the blockchain.

What you'll learn:

  • How CRE's secure write mechanism works (and why it's different from traditional web3)
  • What a consumer contract is and why you need one
  • Which approach to use based on your specific use case
  • How to construct Solidity-compatible types in Go

Understanding how CRE writes work

Before diving into code, it's important to understand how CRE handles onchain writes differently than traditional web3 applications.

Why CRE doesn't write directly to your contract

In a traditional web3 app, you'd create a transaction and send it directly to your smart contract. CRE uses a different, more secure approach for three key reasons:

  1. Decentralization: Multiple nodes in the Decentralized Oracle Network (DON) need to agree on what data to write
  2. Verification: The blockchain needs cryptographic proof that the data came from a trusted Chainlink network
  3. Accountability: There must be a verifiable trail showing which workflow and owner created the data

The secure write flow (4 steps)

Here's the journey your workflow's data takes to reach the blockchain:

  1. Report generation: Your workflow generates a report— your data is ABI-encoded and wrapped in a cryptographically signed "package"
  2. DON consensus: The DON reaches consensus on the report's contents
  3. Forwarder submission: A designated node submits the report to a Chainlink KeystoneForwarder contract
  4. Delivery to your contract: The Forwarder validates the report's signatures and calls your consumer contract's onReport() function with the data

Your workflow code handles this process using the evm.Client, which manages the interaction with the Forwarder contract. Depending on your approach (covered below), this can be fully automated via generated binding helpers or done manually with direct client calls.

What you need: A consumer contract

Before you can write data onchain, you need a consumer contract. This is the smart contract that will receive your workflow's data.

What is a consumer contract?

A consumer contract is your smart contract that implements the IReceiver interface. This interface defines an onReport() function that the Chainlink Forwarder calls to deliver your workflow's data.

Think of it as a mailbox that's designed to receive packages (reports) from Chainlink's secure delivery service (the Forwarder contract).

Key requirement:

Your contract must implement the IReceiver interface. This single requirement ensures your contract has the necessary onReport(bytes metadata, bytes report) function that the Chainlink Forwarder calls to deliver data.

Getting started:

  • Don't have a consumer contract yet? Follow the Building Consumer Contracts guide to create one.
  • Already have one deployed? Great! Make sure you have its address ready. Depending on which approach you choose (see below), you may also need the contract's ABI to generate bindings.

Choosing your approach: Which guide should you follow?

Now that you have a consumer contract, the next step depends on what type of data you're sending and what's available in your contract's ABI. This determines whether you can use the easy automated approach or need to encode data manually.

Use this table to find the guide that matches your needs:

Your scenario
What you have
Recommended approachWhere to go
Write a struct onchainStruct is in the ABI(*)Use the WriteReportFrom<Struct> binding helperUsing WriteReportFrom Helpers
Write a struct onchainStruct is NOT in the ABI(*)
  • Manual tuple encoding
  • Report generation
  • Report submission
Write a single value onchainNeed to send one uint256, address, bool, etc.
  • Manual ABI encoding
  • Report generation
  • Report submission
Already have a generated report and need to submit it onchainA report from runtime.GenerateReport()Manual submission with evm.ClientSubmitting Reports Onchain

(*) When is a struct included in the ABI?

Your contract's ABI includes a struct's definition if that struct is used anywhere in the signature (as a parameter or a return value) of a public or external function.

For example, this contract's ABI will include the CalculatorResult struct:

contract MyConsumerContract {
  struct CalculatorResult {
    uint256 offchainValue;
    int256 onchainValue;
    uint256 finalResult;
  }

  // The struct is used as a parameter in a public function - it WILL be in the ABI
  function isResultAnomalous(CalculatorResult memory _prospectiveResult) public view returns (bool) {
    // ...
  }

  // The struct is used as a return value in a public function - it WILL also be in the ABI
  function getSampleResult() public pure returns (CalculatorResult memory) {
    return CalculatorResult(1, 2, 3);
  }

  // ...
}

Why does this matter? When you compile your contract, only public and external functions and their signatures are included in the ABI file. If a struct is part of that signature, its definition is also included so that external applications know how to encode and decode it. The CRE binding generator reads the ABI and creates helper methods for any structs it finds there.

What if my struct is only used internally? If your struct is only used in internal/private functions, or only used via abi.decode inside functions that take bytes, it won't be in the ABI. In that case, use the Generating Reports: Structs guide for manual encoding.

Working with Solidity input types

Before writing data to a contract, you often need to convert or construct values from your workflow's configuration and logic into the types that Solidity expects. This section explains the common type conversions you'll encounter when preparing your data.

Converting strings to addresses

Contract addresses are typically stored as strings in your config.json file. To use them with generated bindings, convert them to common.Address:

import "github.com/ethereum/go-ethereum/common"

// From a config string
contractAddress := common.HexToAddress(config.ProxyAddress)
// contractAddress is now a common.Address

// Use it directly with bindings
contract, err := my_contract.NewMyContract(evmClient, contractAddress, nil)

Creating big.Int values

All Solidity integer types (uint8, uint256, int8, int256, etc.) map to Go's *big.Int. Here are the common ways to create them:

From an integer literal:

import "math/big"

// For small values, use big.NewInt()
gasLimit := big.NewInt(1000000)
amount := big.NewInt(100)

From a string (for large numbers):

// For values too large for int64, parse from a string
largeAmount := new(big.Int)
largeAmount.SetString("1000000000000000000000000", 10) // Base 10

// Or in one line
value, ok := new(big.Int).SetString("123456789", 10)
if !ok {
    return fmt.Errorf("failed to parse big.Int")
}

From calculations:

// Arithmetic with big.Int
a := big.NewInt(100)
b := big.NewInt(50)

sum := new(big.Int).Add(a, b)
product := new(big.Int).Mul(a, b)

From random numbers:

// Get the runtime's random generator
rnd, err := runtime.Rand()
if err != nil {
    return err
}

// Generate a random big.Int in range [0, max)
max := big.NewInt(1000)
randomValue := new(big.Int).Rand(rnd, max)

Note: For a complete understanding of how randomness works in CRE, including the difference between DON mode and Node mode randomness, see Random in CRE.

Constructing input structs

When your contract method takes parameters, you'll need to construct the input struct generated by the bindings. The binding generator creates a struct type for each method that has parameters.

// Example: For a method that takes (address owner, address spender)
// The generator creates an AllowanceInput struct
allowanceInput := ierc20.AllowanceInput{
    Owner:   common.HexToAddress("0xOwnerAddress"),
    Spender: common.HexToAddress("0xSpenderAddress"),
}
// This struct can now be passed to the corresponding method

Working with bytes

Solidity types like bytes and bytes32 map to []byte in Go.

Learn more

Get the latest Chainlink content straight to your inbox.