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:
- Decentralization: Multiple nodes in the Decentralized Oracle Network (DON) need to agree on what data to write
- Verification: The blockchain needs cryptographic proof that the data came from a trusted Chainlink network
- 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:
- Report generation: Your workflow generates a reportā your data is ABI-encoded and wrapped in a cryptographically signed "package"
- DON consensus: The DON reaches consensus on the report's contents
- Forwarder submission: A designated node submits the report to a Chainlink
KeystoneForwardercontract - 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 approach | Where to go |
|---|---|---|---|
| Write a struct onchain | Struct is in the ABI(*) | Use the WriteReportFrom<Struct> binding helper | Using WriteReportFrom Helpers |
| Write a struct onchain | Struct is NOT in the ABI(*) |
| |
| Write a single value onchain | Need to send one uint256, address, bool, etc. |
| |
| Already have a generated report and need to submit it onchain | A report from runtime.GenerateReport() | Manual submission with evm.Client | Submitting 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
- Building Consumer Contracts: How to create a compliant contract to receive data onchain
- Generating Bindings: How to create type-safe contract bindings
- Using WriteReportFrom Helpers: Implementation guide for the most common approach
- EVM Client Reference: Complete API documentation for the
evm.Client - Onchain Read: Reading data from smart contracts