Receive a Message
On the destination chain, your contract receives Concero messages through conceroReceive(...), which is called by the ConceroRouter after the relayer submits the message and validators are checked.
Concero provides two base contracts to make this safe and consistent:
-
ConceroClientBase: minimal base that enforces router-only calls, allowed relayer libs, and lets you define your own validator policy. -
ConceroClient: an opinionated base that enforces a strict consensus policy (all required validators must approve + allowlist).
You implement two hooks:
-
_validateMessageSubmission(...)— your security policy (how you interpret validator results) -
_conceroReceive(messageReceipt)— your application logic (decode the receipt and use the payload)
What gets called on the destination chain
External entrypoint (called by the router)
function conceroReceive(
bytes calldata messageReceipt,
bool[] calldata validationChecks,
address[] calldata validatorLibs,
address relayerLib
) external;You generally do not override this in app code. ConceroClientBase already implements it and performs critical checks before calling your internal hook.
Built-in safety checks (ConceroClientBase)
ConceroClientBase.conceroReceive(...) enforces:
1) Only the trusted router can deliver
require(msg.sender == i_conceroRouter, InvalidConceroRouter(msg.sender));This prevents anyone else from spoofing deliveries.
2) Only allowed relayer libs can be used
require(s_conceroClient.isRelayerLibAllowed[relayerLib], UnauthorizedRelayerLib(relayerLib));Even if a user chose some relayerLib on the source chain, your app decides which relayer libs it accepts on the destination chain.
3) Your validator policy must pass
_validateMessageSubmission(validationChecks, validatorLibs);4) Then your business logic runs
_conceroReceive(messageReceipt);Choosing a validation policy
Option A — Use ConceroClient (strict consensus)
ConceroClient requires:
requiredValidatorsCountis set (> 0)validationChecks.length == validatorLibs.length == requiredValidatorsCount- every
validationChecks[i]istrue - every
validatorLibs[i]is allowlisted (isValidatorAllowed[validatorLibs[i]] == true)
This is the safest default if you want a simple rule: exactly N validators, all must approve.
Option B — Implement your own policy in ConceroClientBase
For example:
-
“2 of 3 validators must approve”
-
“validator A required, plus at least one of B,C”
-
“accept if ANY validator approves” (not recommended, but possible)
How to read message data
Your app logic receives a single bytes calldata messageReceipt. Use MessageCodec helpers to decode what you need.
Common fields:
uint24 srcChainSelector = messageReceipt.srcChainSelector();
uint256 nonce = messageReceipt.nonce();
(address srcSender, uint64 srcBlockConfirmations) = messageReceipt.evmSrcChainData();
(address receiver, uint32 gasLimit) = messageReceipt.evmDstChainData();
bytes memory relayerConfig = messageReceipt.relayerConfig();
bytes memory payload = messageReceipt.payload();Example: Minimal receiver using ConceroClient + decoding payload
This example:
-
accepts deliveries only from the trusted router,
-
allows only a specific
relayerLib, -
requires strict validator consensus (via
ConceroClient), -
decodes the source sender and payload.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {ConceroClient} from "./ConceroClient.sol";
import {MessageCodec} from "../common/libraries/MessageCodec.sol";
contract MessageReceiver is ConceroClient {
using MessageCodec for bytes;
event ConceroMessageAccepted(
bytes32 indexed messageId,
uint24 indexed srcChainSelector,
address indexed srcSender,
bytes payload
);
constructor(address conceroRouter) ConceroClient(conceroRouter) {}
/// @dev Your app logic: decode receipt + handle payload
function _conceroReceive(bytes calldata messageReceipt) internal override {
// Identify the message
bytes32 messageId = keccak256(messageReceipt);
uint24 srcChainSelector = messageReceipt.srcChainSelector();
// EVM source sender (packed address + confirmations in srcChainData)
(address srcSender, ) = messageReceipt.evmSrcChainData();
// The actual delivered bytes for your app
bytes calldata payload = messageReceipt.calldataPayload();
// Your checks / routing / decoding go here:
// e.g. (uint256 amount, address to) = abi.decode(payload, (uint256, address));
emit ConceroMessageAccepted(messageId, srcChainSelector, srcSender, payload);
}
}Integration checklist
- Set the correct router address in the constructor (destination chain router).
- Allow the relayer lib(s) you want to trust (today typically: allow only concero).
- Choose and configure your validator policy:
- if using
ConceroClient, set:requiredValidatorsCount- allowed validator libs
- if using
- In
_conceroReceive, treat payload as untrusted input: - decode carefully,
- validate source chain selector and/or source sender if your app needs it,
- keep gas usage predictable (payload can be large).