The sender contract calls
syncState on the StateSender contract on Ethereum. This emits a StateSynced event that Heimdall validators pick up and relay to Bor.0x49E307Fa5a58ff1834E0F8a60eB2a9609E6A5F50 on Sepolia0x28e4F3a7f651294B9564800b2D01f35189A5bFbE on Ethereum mainnetpragma solidity ^0.8.0;
interface IStateSender {
function syncState(address receiver, bytes calldata data) external;
}
contract Sender {
address public stateSenderContract;
address public receiver;
uint public states;
constructor(address _stateSender, address _receiver) {
stateSenderContract = _stateSender;
receiver = _receiver;
}
function sendState(bytes calldata data) external {
states += 1;
IStateSender(stateSenderContract).syncState(receiver, data);
}
}
Deploy
Sender.sol to Sepolia, passing in the StateSender address and your receiver contract address. Note the deployed address and ABI.The receiver contract is invoked by the
StateReceiver system contract on Bor whenever a matching StateSynced event is processed.Verify the caller
Always check that
msg.sender is the StateReceiver contract at 0x0000000000000000000000000000000000001001. Without this check, any address on Polygon can call onStateReceive on your contract directly.pragma solidity ^0.8.0;
interface IStateReceiver {
function onStateReceive(uint256 stateId, bytes calldata data) external;
}
contract Receiver is IStateReceiver {
address constant STATE_RECEIVER = 0x0000000000000000000000000000000000001001;
uint public lastStateId;
bytes public lastData;
function onStateReceive(uint256 stateId, bytes calldata data) external {
require(msg.sender == STATE_RECEIVER, "unauthorized");
lastStateId = stateId;
lastData = data;
}
}
stateId is a monotonically increasing counter that uniquely identifies each state sync event. Deploy Receiver.sol to Amoy and note the deployed address and ABI.Before state sync will trigger your receiver, your sender/receiver pair must be registered with the
StateSender contract. This registration is what authorizes the pair: only events originating from a registered sender contract targeting a registered receiver will be relayed by Heimdall validators.Registration for new custom sender/receiver pairs is managed by the Polygon team. Contact the team on Discord or submit a request via the mapping form to initiate the process.
If you want to test end-to-end before your custom contracts are registered, you can use the already-registered example contracts referenced in the form.
With contracts deployed and registered, use the following script to send arbitrary bytes from Sepolia and verify receipt on Amoy. State sync takes approximately 7 to 8 minutes to complete.
const { Web3 } = require('web3');
const main = new Web3('<SEPOLIA_RPC_URL>');
const matic = new Web3('<AMOY_RPC_URL>');
const privateKey = '0x...';
main.eth.accounts.wallet.add(privateKey);
const senderAddress = '<SENDER_CONTRACT_ADDRESS>';
const senderABI = [/* paste ABI here */];
const receiverAddress = '<RECEIVER_CONTRACT_ADDRESS>';
const receiverABI = [/* paste ABI here */];
const sender = new main.eth.Contract(senderABI, senderAddress);
const receiver = new matic.eth.Contract(receiverABI, receiverAddress);
async function sendData(message) {
const data = matic.utils.asciiToHex(message);
const tx = await sender.methods.sendState(data).send({
from: main.eth.accounts.wallet[0].address,
gas: 200000,
});
console.log('Sent from Ethereum:', tx.transactionHash);
}
async function checkReceiver() {
const stateId = await receiver.methods.lastStateId().call();
const rawData = await receiver.methods.lastData().call();
const message = matic.utils.hexToAscii(rawData);
console.log('Last state ID:', stateId);
console.log('Received message:', message);
}
async function run() {
await sendData('Hello from Ethereum!');
console.log('Waiting ~8 minutes for state sync...');
setTimeout(checkReceiver, 8 * 60 * 1000);
}
run();